diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb88a33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.grunt + diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..0c3989c --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": false, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "smarttabs": true, + "strict": true, + "trailing": true, + "undef": true, + "validthis": true +} \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..75a375c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,56 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + wrap: { + basic: { + src: ['build/perfmap.js'], + dest: 'build/perfmap.js', + options: { + wrapper: ['\n;(function(window, document, undefined){\n\'use strict\';\n', '\nwindow.perfMap = perfMap;\n})(window, document);\nperfMap.init();'] + } + } + }, + concat: { + options: { + stripBanners: true + }, + dist: { + src: 'src/**/*.js', + dest: 'build/perfmap.js' + } + }, + jshint: { + //src: ['src/**/*.js'], + src: ['build/perfmap.js'], + options: { + jshintrc: '.jshintrc' + } + }, + jasmine: { + // Your project's source files + src: 'src/**/*.js', + options: { + specs: 'specs/**/*spec.js' + } + }, + lenient: { + src: 'build/perfmap.js', + dest: 'build/perfmap.js' + } + }); + + // Register tasks. + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-wrap'); + grunt.loadNpmTasks('grunt-lenient'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-jasmine'); + + // Default task. + grunt.registerTask('default', ['jasmine', 'concat:dist', 'jshint']); + grunt.registerTask('test', ['jasmine']); + grunt.registerTask('build', ['concat:dist', 'lenient', 'wrap:basic', 'jasmine', 'jshint']); + +}; \ No newline at end of file diff --git a/build/perfmap.js b/build/perfmap.js new file mode 100644 index 0000000..dd3af4c --- /dev/null +++ b/build/perfmap.js @@ -0,0 +1,307 @@ + +;(function(window, document, undefined){ +'use strict'; + +var perfMap = {}, + loading, + loaded, + gZeroLeft = 0, + gZeroTop = 0, + hArr = [{ + threashold: 0.16, + value: '#1a9850', + rgba: 'rgba(26, 152, 80, 0.95)' + }, { + threashold: 0.32, + value: '#66bd63', + rgba: 'rgba(102, 189, 99, 0.95)' + }, { + threashold: 0.48, + value: '#a6d964', + rgba: 'rgba(166, 217, 100, 0.95)' + }, { + threashold: 0.64, + value: '#fdae61', + rgba: 'rgba(253, 174, 97, 0.95)' + }, { + threashold: 0.8, + value: '#f46d43', + rgba: 'rgba(244, 109, 67, 0.95)' + }, { + threashold: 1.1, + value: '#d73027', + rgba: 'rgba(215, 48, 39, 0.95)' + }]; + + +function findImages() { + var tags = document.getElementsByTagName('*'), + images = document.getElementsByTagName('img'), + el, + len, + imgs = []; + //re = /url\(([http].*)\)/ig; + //re = /(url)\((.*?)\)/ig; + + imgs = getTagImages(document); + len = tags.length; + for (var j = 0; j < len; j++) { + el = getBgElement(tags[j]); + if (!!el && !!el.src) { + el.bgImg = el.src; + + var match = el.src.match(/\((.*?)\)/); + if (match[1]) { + el.src = match[1].replace(/('|")/g, ''); + /*if (style['visibility'] == "hidden") { + hasImage = 0; + } else { + hasImage = 1; + if (elem.tagName == 'BODY') { + body = 1; + } + imgs.push(el); + }*/ + imgs.push(el); + } + } + } + + + //console.log(imgs); + len = imgs.length; + for (var i = 0; i < len; i++) { + var entry = window.performance.getEntriesByName(imgs[i].src)[0]; + if (entry) { + //var xy = getCumulativeOffset(imgs[i].element, imgs[i].src); + var wh = imgs[i].element.getBoundingClientRect(); + var width = wh.width; + var height = wh.height; + if (width > 10) { + if (height > 10) { + placeMarker(width, height, entry, imgs[i].element.tagName === 'BODY', imgs[i].src, imgs[i]); + } + } + } + } +} + + +function getBgElement(el) { + /*jshint sub: true */ + //console.log(el.className); + var style = el.currentStyle || window.getComputedStyle(el, false); + //console.log(style.getPropertyValue('background-image')); + if (style.getPropertyValue('background-image') !== 'none') { + return { + element: el, + src: style.getPropertyValue('background-image'), + position: style.getPropertyValue('background-position') + }; + } + return null; +} + + +/*function getCumulativeOffset(obj, url) { + var left, top; + left = top = 0; + if (obj.offsetParent) { + do { + left += obj.offsetLeft; + top += obj.offsetTop; + } while (obj = obj.offsetParent); + } + if (0 === top) { + left += gZeroLeft; + top += gZeroTop; + } + return { + left: left, + top: top, + }; +}*/ + + +function getTagImages(document) { + var images = document.getElementsByTagName('img'), + imgs = []; + for (var i = 0; i < images.length; i++) { + if (!!images[i].src) { + imgs.push({ + element: images[i], + src: images[i].src + }); + } + } + return imgs; +} + + +function heatmap(heat) { + function findIndex(array, predicate) { + var index = -1, + length = array ? array.length : 0; + while (++index < length) { + if (predicate(array[index], index, array)) { + return array[index]; + } + } + return array[length - 1]; + } + + return findIndex(hArr, function(chr) { + return heat < chr.threashold; + }); +} + + +perfMap.init = function() { + startLoading(); + // build heatmap + findImages(); + + // remove loading message + loading.remove(); + + // mouse events to move timeline around on hover + var elements = document.getElementsByClassName('perfmap'); + var timeline = document.getElementById('perfmap-timeline'); + var timelineLeft; + for (var i = 0, len = elements.length; i < len; i++) { + elements[i].onmouseover = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body !=='1') { + //this.style.opacity = 1; + this.style.cssText = this.style.cssText.replace(/(\d\.\d*)\)/g, '0.1)'); + timeline.style.cssText = 'opacity:1; transition: 0.5s ease-in-out; transform: translate(' + parseInt(timelineLeft) + 'px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;'; + } + + }; + elements[i].onmouseout = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body !== '1') { + //this.style.opacity = 0.925; + this.style.cssText = this.style.cssText.replace(/(\d\.\d*)\)/g, '0.95)'); + timeline.style.cssText = 'opacity:0; transition: 0.5s ease-in-out; transform: translate(' + parseInt(timelineLeft) + 'px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;'; + } + + }; + } +}; + + +function placeMarker(width, height, entry, body, url, el) { + //background-image: linear-gradient(90deg, rgba(253, 174, 97, 0.95), rgba(253, 174, 97, 0.95)); + var heat = (entry.responseEnd / loaded), + marker = document.createElement('div'), + padding = 9, + size = 18, + paddingTop, + opacity = 0.925, + align = 'center', + bodyText = ''; + + // adjust size of fonts/padding based on width of overlay + if (width < 170) { + padding = 12; + size = 12; + } else if (width > 400) { + padding = 13; + size = 26; + } + + // check for overlay that matches viewport and assume it's like a background image on body + if ((width === document.documentElement.clientWidth) && (height >= document.documentElement.clientHeight)) { + body = true; + } + + // adjust opacity if it's the body element and position label top right + paddingTop = (height / 2) - padding; + if (!!body) { + opacity = 0.6; + size = 18; + align = 'right'; + paddingTop = 10; + bodyText = 'BODY '; + } + var elem = el.element; + //debugger; + var oldClass = elem.className; + elem.className = oldClass + ' perfmap'; + elem.setAttribute('data-ms', parseInt(entry.responseEnd)); + elem.setAttribute('data-body', (body ? 1 : 0)); + var oldStyle = elem.style.cssText; + var bgImg = ''; + if (!!el.bgImg) { + bgImg = ', ' + el.bgImg; + } else { + //debugger; + bgImg = ', url("' + elem.src + '") '; + + var style = elem.currentStyle || window.getComputedStyle(elem, false); + //console.log(style.getPropertyValue('background-image')); + oldStyle += 'width: ' + (style.getPropertyValue('width') || width + 'px') + '!important;'; + oldStyle += 'height: ' + ( style.getPropertyValue('height') || height + 'px') + '!important;'; + elem.removeAttribute("src"); + } + var bgPosition = 'background-position: 0px 0px'; + if (!!el.position) { + bgPosition += ', ' + el.position; + } + + elem.style.cssText = oldStyle + ' background-image: linear-gradient(' + heatmap(heat).rgba + ', ' + heatmap(heat).rgba + ')' + bgImg + '; ' + bgPosition + '; background-size: contain;'; + //elem.style.cssText = 'position:absolute; transition: 0.5s ease-in-out; box-sizing: border-box; color: #fff; padding-left:10px; padding-right:10px; line-height:14px; font-size: ' + size + 'px; font-weight:800; font-family:"Helvetica Neue",sans-serif; text-align:' + align + '; opacity: ' + opacity + '; background: ' + heatmap(heat).value + '; top: ' + xy.top + 'px; left: ' + xy.left + 'px; width: ' + width + 'px; height:' + height + 'px; padding-top:' + paddingTop + 'px; z-index: 4000;'; + // if (width > 50) { + // if (height > 15) { + // oldStyle = elem.style.cssText; + // elem.style.cssText = oldStyle + ' content: "' + bodyText + parseInt(entry.responseEnd) + 'ms (' + parseInt(entry.duration) + 'ms)";'; + // } + // } + //document.body.appendChild(marker); +} + + +function startLoading() { + + // give visual feedback asap + loading = document.createElement('div'); + loading.id = 'perfmap-loading'; + loading.innerHTML = 'Creating PerfMap'; + loading.style.cssText = 'position:absolute; z-index:6000; left:40%; top:45%; background-color:#000; color:#fff; padding:20px 30px; font-family:"Helvetica Neue",sans-serif; font-size:24px; font-weight:800;border:2px solid white;'; + document.body.appendChild(loading); + + // get full page load time to calculate heatmap max + loaded = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart; + + // backend + var backend = window.performance.timing.responseEnd - window.performance.timing.navigationStart; + var backendLeft = (backend / loaded) * 100; + var paint, firstPaint, firstPaintLeft; + + // first paint in chrome from https://github.com/addyosmani/timing.js + if (window.chrome && window.chrome.loadTimes) { + paint = window.chrome.loadTimes().firstPaintTime * 1000; + firstPaint = paint - (window.chrome.loadTimes().startLoadTime * 1000); + firstPaintLeft = (firstPaint / loaded) * 100; + } + + // remove any exisiting "perfmap" divs on second click + var elements = document.getElementsByClassName('perfmap'); + while (elements.length > 0) { + elements[0].parentNode.removeChild(elements[0]); + } + + // build bottom legend + var perfmap = document.createElement('div'); + perfmap.id = 'perfmap'; + perfmap.style.cssText = 'position: fixed; width:100%; bottom:0; left:0; z-index:5000; height: 25px; color:#fff; font-family:"Helvetica Neue",sans-serif; font-size:14px; font-weight:800; line-height:14px;'; + perfmap.innerHTML = '
Fully Loaded ' + parseInt(loaded) + 'ms
First Paint ' + parseInt(firstPaint) + 'ms
'; + document.body.appendChild(perfmap); + + +} + +window.perfMap = perfMap; +})(window, document); +perfMap.init(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b412c19 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "perfMap", + "version": "0.0.1", + "devDependencies": { + "grunt": "^0.4.5", + "grunt-contrib-concat": "^0.5.0", + "grunt-contrib-jasmine": "^0.8.0", + "grunt-contrib-jshint": "^0.10.0", + "grunt-lenient": "0.0.2", + "grunt-wrap": "^0.3.0", + "phantomjs": "^1.9.11" + } +} diff --git a/perfmap.js b/perfmap.js index 5dbb72e..c797e49 100644 --- a/perfmap.js +++ b/perfmap.js @@ -1,195 +1,261 @@ -var gZeroLeft = 0; -var gZeroTop = 0; -var gWinWidth = window.innerWidth || document.documentElement.clientWidth; - -function findImages() { - var aElems = document.getElementsByTagName('*'); - var re = /url\((http.*)\)/ig; - for ( var i=0, len = aElems.length; i < len; i++ ) { - var elem = aElems[i]; - var style = window.getComputedStyle(elem); - var url = elem.src || elem.href; - var hasImage = 0; - var fixed = 0; - var body = 0; - re.lastIndex = 0; // reset state of regex so we catch repeating spritesheet elements - if (elem.tagName == 'IMG') { - hasImage = 1; - } - if (style['background-image']) { - var backgroundImage = style['background-image']; - var matches = re.exec(style['background-image']); - if (matches && matches.length > 1){ - url = backgroundImage.substring(4); - url = url.substring(0, url.length - 1); - hasImage = 1; - if(elem.tagName == 'BODY'){ - body = 1; - } - } - } - if (style['visibility'] == "hidden") { - hasImage = 0; - } - if(hasImage == 1){ - if ( url ) { - var entry = performance.getEntriesByName(url)[0]; - if ( entry ) { - var xy = getCumulativeOffset(elem, url); - var wh = elem.getBoundingClientRect(); - var width = wh.width; - var height = wh.height; - if(width > 10){ - if(height > 10){ - placeMarker(xy, width, height, entry, body, url); - } - } - } - } - } - } -} - -function placeMarker(xy, width, height, entry, body, url) { - var heat = entry.responseEnd / loaded; - // adjust size of fonts/padding based on width of overlay - if(width < 170){ - var padding = 12; - var size = 12; - }else if(width > 400){ - var padding = 13; - var size = 26; - }else{ - var padding = 9; - var size = 18; - } - // check for overlay that matches viewport and assume it's like a background image on body - if(width == document.documentElement.clientWidth){ - if(height >= document.documentElement.clientHeight){ - body = 1; - } - } - // adjust opacity if it's the body element and position label top right - if(body == 1){ - var opacity = 0.6; - var size = 18; - var align = "right"; - var paddingTop = 10; - var bodyText = "BODY "; - }else{ - var opacity = 0.925; - var align = "center"; - var paddingTop = (height/2)-padding; - var bodyText = ""; - } - var marker = document.createElement("div"); - marker.className = "perfmap"; - marker.setAttribute("data-ms", parseInt(entry.responseEnd)); - marker.setAttribute("data-body", body); - marker.style.cssText = "position:absolute; transition: 0.5s ease-in-out; box-sizing: border-box; color: #fff; padding-left:10px; padding-right:10px; line-height:14px; font-size: " + size + "px; font-weight:800; font-family:\"Helvetica Neue\",sans-serif; text-align:" + align + "; opacity: " + opacity + "; " + heatmap(heat) + " top: " + xy.top + "px; left: " + xy.left + "px; width: " + width + "px; height:" + height + "px; padding-top:" + paddingTop + "px; z-index: 4000;"; - if(width > 50){ - if(height > 15 ){ - marker.innerHTML = bodyText + parseInt(entry.responseEnd) + "ms (" + parseInt(entry.duration) + "ms)"; - } - } - document.body.appendChild(marker); -} +(function(window, document, undefined) { + 'use strict'; + var perfMap = {}; -function heatmap(heat) { - if ( heat < 0.16 ) { - return "background: #1a9850;" - } - else if ( heat < 0.32 ) { - return "background: #66bd63;" - } - else if ( heat < 0.48 ) { - return "background: #a6d96a;" + + var loading, + loaded, + gZeroLeft = 0, + gZeroTop = 0, + hArr = [{ + threashold: 0.16, + value: "#1a9850" + }, { + threashold: 0.32, + value: "#66bd63" + }, { + threashold: 0.48, + value: "#a6d964" + }, { + threashold: 0.64, + value: "#fdae61" + }, { + threashold: 0.8, + value: "#f46d43" + }, { + threashold: 1.1, + value: "#d73027" + }]; + + function findImages() { + var tags = document.getElementsByTagName('*'), + images = document.getElementsByTagName('img'), + el, + imgs = []; + //re = /url\(([http].*)\)/ig; + //re = /(url)\((.*?)\)/ig; + + function getBgElement(el) { + if (!!el.currentStyle) { + if (el.currentStyle['backgroundImage'] !== 'none') { + //el.className += ' bg_found'; + return { + element: el, + src: el.currentStyle['backgroundImage'] + }; + } + } else if (!!window.getComputedStyle) { + if (document.defaultView.getComputedStyle(el, null).getPropertyValue('background-image') !== 'none') { + //el.className += ' bg_found'; + return { + element: el, + src: document.defaultView.getComputedStyle(el, null).getPropertyValue('background-image') + }; + } + } + } + + for (var i = 0, len = tags.length; i < len; i++) { + //imgs.push(getBgElement(tags[i])); + var el = getBgElement(tags[i]); + if (!!el && !!el.src) { + var match = el.src.match(/\((.*?)\)/); + if (match[1]) { + el.src = match[1].replace(/('|")/g, ''); + /*if (style['visibility'] == "hidden") { + hasImage = 0; + } else { + hasImage = 1; + if (elem.tagName == 'BODY') { + body = 1; + } + imgs.push(el); + }*/ + imgs.push(el); + } + } + } + + for (var i = 0; i < images.length; i++) { + imgs.push({ + element: images[i], + src: images[i].src + }); + } + + //console.log(imgs); + for (var i = 0, len = imgs.length; i < len; i++) { + var entry = window.performance.getEntriesByName(imgs[i].src)[0]; + if (entry) { + var xy = getCumulativeOffset(imgs[i].element, imgs[i].src); + var wh = imgs[i].element.getBoundingClientRect(); + var width = wh.width; + var height = wh.height; + if (width > 10) { + if (height > 10) { + placeMarker(xy, width, height, entry, imgs[i].element.tagName === 'BODY', imgs[i].src); + } + } + } + } } - else if ( heat < 0.64 ) { - return "background: #fdae61;" + + + function placeMarker(xy, width, height, entry, body, url) { + var heat = (entry.responseEnd / loaded), + marker = document.createElement("div"), + padding = 9, + size = 18, + paddingTop, + opacity = 0.925, + align = "center", + bodyText = ""; + + // adjust size of fonts/padding based on width of overlay + if (width < 170) { + padding = 12; + size = 12; + } else if (width > 400) { + padding = 13; + size = 26; + } + + // check for overlay that matches viewport and assume it's like a background image on body + if ((width === document.documentElement.clientWidth) && (height >= document.documentElement.clientHeight)) { + body = true; + } + + // adjust opacity if it's the body element and position label top right + paddingTop = (height / 2) - padding; + if (!!body) { + opacity = 0.6; + size = 18; + align = "right"; + paddingTop = 10; + bodyText = "BODY "; + } + + marker.className = "perfmap"; + marker.setAttribute("data-ms", parseInt(entry.responseEnd)); + marker.setAttribute("data-body", (body ? 1 : 0)); + marker.style.cssText = "position:absolute; transition: 0.5s ease-in-out; box-sizing: border-box; color: #fff; padding-left:10px; padding-right:10px; line-height:14px; font-size: " + size + "px; font-weight:800; font-family:\"Helvetica Neue\",sans-serif; text-align:" + align + "; opacity: " + opacity + "; background: " + heatmap(heat).value + "; top: " + xy.top + "px; left: " + xy.left + "px; width: " + width + "px; height:" + height + "px; padding-top:" + paddingTop + "px; z-index: 4000;"; + if (width > 50) { + if (height > 15) { + marker.innerHTML = bodyText + parseInt(entry.responseEnd) + "ms (" + parseInt(entry.duration) + "ms)"; + } + } + document.body.appendChild(marker); } - else if ( heat < 0.8 ) { - return "background: #f46d43;" - }else{ - return "background: #d73027;" + + function heatmap(heat) { + function findIndex(array, predicate) { + var index = -1, + length = array ? array.length : 0; + while (++index < length) { + if (predicate(array[index], index, array)) { + return array[index]; + } + } + return array[length - 1]; + } + + return findIndex(hArr, function(chr) { + return heat < chr.threashold; + }); } -} - -function getCumulativeOffset(obj, url) { - var left, top; - left = top = 0; - if (obj.offsetParent) { - do { - left += obj.offsetLeft; - top += obj.offsetTop; - } while (obj = obj.offsetParent); + + function getCumulativeOffset(obj, url) { + var left, top; + left = top = 0; + if (obj.offsetParent) { + do { + left += obj.offsetLeft; + top += obj.offsetTop; + } while (obj = obj.offsetParent); + } + if (0 === top) { + left += gZeroLeft; + top += gZeroTop; + } + return { + left: left, + top: top, + }; } - if ( 0 == top ) { - left += gZeroLeft; - top += gZeroTop; + + function startLoading() { + + // give visual feedback asap + loading = document.createElement("div"); + loading.id = "perfmap-loading"; + loading.innerHTML = "Creating PerfMap"; + loading.style.cssText = "position:absolute; z-index:6000; left:40%; top:45%; background-color:#000; color:#fff; padding:20px 30px; font-family:\"Helvetica Neue\",sans-serif; font-size:24px; font-weight:800;border:2px solid white;"; + document.body.appendChild(loading); + + // get full page load time to calculate heatmap max + loaded = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart; + + // backend + var backend = window.performance.timing.responseEnd - window.performance.timing.navigationStart; + var backendLeft = (backend / loaded) * 100; + var paint, firstPaint, firstPaintLeft; + + // first paint in chrome from https://github.com/addyosmani/timing.js + if (window.chrome && window.chrome.loadTimes) { + paint = window.chrome.loadTimes().firstPaintTime * 1000; + firstPaint = paint - (window.chrome.loadTimes().startLoadTime * 1000); + firstPaintLeft = (firstPaint / loaded) * 100; + } + + // remove any exisiting "perfmap" divs on second click + var elements = document.getElementsByClassName("perfmap"); + while (elements.length > 0) { + elements[0].parentNode.removeChild(elements[0]); + } + + // build bottom legend + var perfmap = document.createElement("div"); + perfmap.id = "perfmap"; + perfmap.style.cssText = "position: fixed; width:100%; bottom:0; left:0; z-index:5000; height: 25px; color:#fff; font-family:\"Helvetica Neue\",sans-serif; font-size:14px; font-weight:800; line-height:14px;"; + perfmap.innerHTML = "
Fully Loaded " + parseInt(loaded) + "ms
First Paint " + parseInt(firstPaint) + "ms
"; + document.body.appendChild(perfmap); + + } - return { - left: left, - top: top, + + + + perfMap.init = function() { + startLoading(); + // build heatmap + findImages(); + + // remove loading message + loading.remove(); + + // mouse events to move timeline around on hover + var elements = document.getElementsByClassName("perfmap"); + var timeline = document.getElementById('perfmap-timeline'); + var timelineLeft; + for (var i = 0, len = elements.length; i < len; i++) { + elements[i].onmouseover = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body != "1") { + this.style.opacity = 1; + } + timeline.style.cssText = "opacity:1; transition: 0.5s ease-in-out; transform: translate(" + parseInt(timelineLeft) + "px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;"; + }; + elements[i].onmouseout = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body != "1") { + this.style.opacity = 0.925; + } + timeline.style.cssText = "opacity:0; transition: 0.5s ease-in-out; transform: translate(" + parseInt(timelineLeft) + "px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;"; + }; + } }; -} - -// give visual feedback asap -var loading = document.createElement("div"); -loading.id = "perfmap-loading"; -loading.innerHTML = "Creating PerfMap"; -loading.style.cssText = "position:absolute; z-index:6000; left:40%; top:45%; background-color:#000; color:#fff; padding:20px 30px; font-family:\"Helvetica Neue\",sans-serif; font-size:24px; font-weight:800;border:2px solid white;"; -document.body.appendChild(loading); - -// get full page load time to calculate heatmap max -var loaded = performance.timing.loadEventEnd - performance.timing.navigationStart; - -// backend -var backend = performance.timing.responseEnd - performance.timing.navigationStart; -var backendLeft = (backend / loaded)*100; - -// first paint in chrome from https://github.com/addyosmani/timing.js -if (window.chrome && window.chrome.loadTimes) { - var paint = window.chrome.loadTimes().firstPaintTime * 1000; - var firstPaint = paint - (window.chrome.loadTimes().startLoadTime*1000); - var firstPaintLeft = (firstPaint / loaded)*100; -} - -// remove any exisiting "perfmap" divs on second click -var elements = document.getElementsByClassName("perfmap"); -while(elements.length > 0){ - elements[0].parentNode.removeChild(elements[0]); -} - -// build bottom legend -var perfmap = document.createElement("div"); -perfmap.id = "perfmap"; -perfmap.style.cssText = "position: fixed; width:100%; bottom:0; left:0; z-index:5000; height: 25px; color:#fff; font-family:\"Helvetica Neue\",sans-serif; font-size:14px; font-weight:800; line-height:14px;"; -perfmap.innerHTML = "
Fully Loaded " + parseInt(loaded) + "ms
First Paint " + parseInt(firstPaint) + "ms
"; -document.body.appendChild(perfmap); - -// build heatmap -findImages(); - -// remove loading message -loading.remove(); - -// mouse events to move timeline around on hover -var elements = document.getElementsByClassName("perfmap"); -var timeline = document.getElementById('perfmap-timeline'); -for ( var i=0, len = elements.length; i < len; i++ ) { - elements[i].onmouseover = function(){ - var timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); - if(this.dataset.body != "1"){ - this.style.opacity = 1; - } - timeline.style.cssText = "opacity:1; transition: 0.5s ease-in-out; transform: translate("+ parseInt(timelineLeft) + "px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;"; - } - elements[i].onmouseout = function(){ - var timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); - if(this.dataset.body != "1"){ - this.style.opacity = 0.925; - } - timeline.style.cssText = "opacity:0; transition: 0.5s ease-in-out; transform: translate("+ parseInt(timelineLeft) + "px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;"; - } -} \ No newline at end of file + + window.perfMap = perfMap; +})(window, document); + +perfMap.init(); \ No newline at end of file diff --git a/specs/getBgImages.spec.js b/specs/getBgImages.spec.js new file mode 100644 index 0000000..eba102b --- /dev/null +++ b/specs/getBgImages.spec.js @@ -0,0 +1,80 @@ +'use strict'; +describe('getBgElement spec', function() { + var dummyDOM = '
' + + 'some alt' + + 'some alt' + + 'some alt' + + '' + + '
'; + + + + it("should return 1 element with same src url", function() { + var div = document.createElement("div"); + div.style.width = "100px"; + div.style.height = "100px"; + div.style.background = "url(http://somePath.com/jasmine.jpg)"; + div.style.color = "white"; + div.innerHTML = "Hello"; + + document.body.appendChild(div); + + var result = getBgElement(div); + expect(result.element).toEqual(div); + expect(result.src).toEqual(div.style.background); + }); + + it("should return null when has backgroung-image = none", function() { + var div = document.createElement("div"); + div.style.width = "100px"; + div.style.height = "100px"; + div.style.background = "none"; + div.style.color = "white"; + div.innerHTML = "Hello"; + + document.body.appendChild(div); + + var result = getBgElement(div); + expect(result).toEqual(null); + }); + it("should return null when has no backgroung", function() { + var div = document.createElement("div"); + div.style.width = "100px"; + div.style.height = "100px"; + + div.style.color = "white"; + div.innerHTML = "Hello"; + + document.body.appendChild(div); + + var result = getBgElement(div); + expect(result).toEqual(null); + }); + + it("should return null when has no backgroung", function() { + var css = '.gb_Fa { background-image: url("https://somePath.com/i1_e9f91fe3.png"); background-size: 247px 204px;}', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); + + + var div = document.createElement("div"); + div.className = "gb_Fa"; + + + document.body.appendChild(div); + + var result = getBgElement(div); + expect(result.element).toEqual(div); + expect(result.src).toEqual('url(https://somePath.com/i1_e9f91fe3.png)'); + }); + + +}); \ No newline at end of file diff --git a/specs/getTagImages.spec.js b/specs/getTagImages.spec.js new file mode 100644 index 0000000..8e80f53 --- /dev/null +++ b/specs/getTagImages.spec.js @@ -0,0 +1,54 @@ +'use strict'; +describe('getTagImages spec', function() { + var container = document.createElement("div"); + container.id = 'container'; + document.body.appendChild(container); + + beforeEach(function() { + document.getElementById('container'); + container.innerHTML = ''; + }); + + it("should return 3 images", function() { + container.innerHTML = '
' + + 'some alt' + + 'some alt' + + 'some alt' + + '' + + '
'; + var result = getTagImages(document); + expect(result.length).toEqual(3); + }); + + it("should return 0 images", function() { + var div = document.createElement("div"); + container.appendChild(div); + var result = getTagImages(document); + expect(result).toEqual([]); + }); + + it("should return 3 images with different extensions", function() { + container.innerHTML = '
' + + 'some alt' + + 'some alt' + + 'some alt' + + '' + + '
'; + var result = getTagImages(document); + expect(result[0].src).toEqual("http://somePath.com/jasmine.png"); + expect(result[1].src).toEqual("http://somePath.com/jasmine.jpg"); + expect(result[2].src).toEqual("http://somePath.com/jasmine.gif"); + }); + + it("should return 0 images when it has no src attribute ", function() { + container.innerHTML = '
' + + 'some alt' + + '' + + '
'; + var result = getTagImages(document); + expect(result.length).toEqual(0); + }); + + + +}); \ No newline at end of file diff --git a/src/_vars.js b/src/_vars.js new file mode 100644 index 0000000..36f36a0 --- /dev/null +++ b/src/_vars.js @@ -0,0 +1,30 @@ +var perfMap = {}, + loading, + loaded, + gZeroLeft = 0, + gZeroTop = 0, + hArr = [{ + threashold: 0.16, + value: '#1a9850', + rgba: 'rgba(26, 152, 80, 0.95)' + }, { + threashold: 0.32, + value: '#66bd63', + rgba: 'rgba(102, 189, 99, 0.95)' + }, { + threashold: 0.48, + value: '#a6d964', + rgba: 'rgba(166, 217, 100, 0.95)' + }, { + threashold: 0.64, + value: '#fdae61', + rgba: 'rgba(253, 174, 97, 0.95)' + }, { + threashold: 0.8, + value: '#f46d43', + rgba: 'rgba(244, 109, 67, 0.95)' + }, { + threashold: 1.1, + value: '#d73027', + rgba: 'rgba(215, 48, 39, 0.95)' + }]; \ No newline at end of file diff --git a/src/findImages.js b/src/findImages.js new file mode 100644 index 0000000..ef6e474 --- /dev/null +++ b/src/findImages.js @@ -0,0 +1,53 @@ +'use strict'; + +function findImages() { + var tags = document.getElementsByTagName('*'), + images = document.getElementsByTagName('img'), + el, + len, + imgs = []; + //re = /url\(([http].*)\)/ig; + //re = /(url)\((.*?)\)/ig; + + imgs = getTagImages(document); + len = tags.length; + for (var j = 0; j < len; j++) { + el = getBgElement(tags[j]); + if (!!el && !!el.src) { + el.bgImg = el.src; + + var match = el.src.match(/\((.*?)\)/); + if (match[1]) { + el.src = match[1].replace(/('|")/g, ''); + /*if (style['visibility'] == "hidden") { + hasImage = 0; + } else { + hasImage = 1; + if (elem.tagName == 'BODY') { + body = 1; + } + imgs.push(el); + }*/ + imgs.push(el); + } + } + } + + + //console.log(imgs); + len = imgs.length; + for (var i = 0; i < len; i++) { + var entry = window.performance.getEntriesByName(imgs[i].src)[0]; + if (entry) { + //var xy = getCumulativeOffset(imgs[i].element, imgs[i].src); + var wh = imgs[i].element.getBoundingClientRect(); + var width = wh.width; + var height = wh.height; + if (width > 10) { + if (height > 10) { + placeMarker(width, height, entry, imgs[i].element.tagName === 'BODY', imgs[i].src, imgs[i]); + } + } + } + } +} \ No newline at end of file diff --git a/src/getBgImages.js b/src/getBgImages.js new file mode 100644 index 0000000..b3b2825 --- /dev/null +++ b/src/getBgImages.js @@ -0,0 +1,16 @@ +'use strict'; + +function getBgElement(el) { + /*jshint sub: true */ + //console.log(el.className); + var style = el.currentStyle || window.getComputedStyle(el, false); + //console.log(style.getPropertyValue('background-image')); + if (style.getPropertyValue('background-image') !== 'none') { + return { + element: el, + src: style.getPropertyValue('background-image'), + position: style.getPropertyValue('background-position') + }; + } + return null; +} \ No newline at end of file diff --git a/src/getCumulativeOffset.js b/src/getCumulativeOffset.js new file mode 100644 index 0000000..9bb0ddb --- /dev/null +++ b/src/getCumulativeOffset.js @@ -0,0 +1,20 @@ +'use strict'; + +/*function getCumulativeOffset(obj, url) { + var left, top; + left = top = 0; + if (obj.offsetParent) { + do { + left += obj.offsetLeft; + top += obj.offsetTop; + } while (obj = obj.offsetParent); + } + if (0 === top) { + left += gZeroLeft; + top += gZeroTop; + } + return { + left: left, + top: top, + }; +}*/ \ No newline at end of file diff --git a/src/getTagImages.js b/src/getTagImages.js new file mode 100644 index 0000000..deb3fb7 --- /dev/null +++ b/src/getTagImages.js @@ -0,0 +1,15 @@ +'use strict'; + +function getTagImages(document) { + var images = document.getElementsByTagName('img'), + imgs = []; + for (var i = 0; i < images.length; i++) { + if (!!images[i].src) { + imgs.push({ + element: images[i], + src: images[i].src + }); + } + } + return imgs; +} \ No newline at end of file diff --git a/src/heatmap.js b/src/heatmap.js new file mode 100644 index 0000000..2d27fcb --- /dev/null +++ b/src/heatmap.js @@ -0,0 +1,18 @@ +'use strict'; + +function heatmap(heat) { + function findIndex(array, predicate) { + var index = -1, + length = array ? array.length : 0; + while (++index < length) { + if (predicate(array[index], index, array)) { + return array[index]; + } + } + return array[length - 1]; + } + + return findIndex(hArr, function(chr) { + return heat < chr.threashold; + }); +} \ No newline at end of file diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..8fdca81 --- /dev/null +++ b/src/init.js @@ -0,0 +1,35 @@ +'use strict'; + +perfMap.init = function() { + startLoading(); + // build heatmap + findImages(); + + // remove loading message + loading.remove(); + + // mouse events to move timeline around on hover + var elements = document.getElementsByClassName('perfmap'); + var timeline = document.getElementById('perfmap-timeline'); + var timelineLeft; + for (var i = 0, len = elements.length; i < len; i++) { + elements[i].onmouseover = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body !=='1') { + //this.style.opacity = 1; + this.style.cssText = this.style.cssText.replace(/(\d\.\d*)\)/g, '0.1)'); + timeline.style.cssText = 'opacity:1; transition: 0.5s ease-in-out; transform: translate(' + parseInt(timelineLeft) + 'px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;'; + } + + }; + elements[i].onmouseout = function() { + timelineLeft = document.documentElement.clientWidth * (this.dataset.ms / loaded); + if (this.dataset.body !== '1') { + //this.style.opacity = 0.925; + this.style.cssText = this.style.cssText.replace(/(\d\.\d*)\)/g, '0.95)'); + timeline.style.cssText = 'opacity:0; transition: 0.5s ease-in-out; transform: translate(' + parseInt(timelineLeft) + 'px,0); position:absolute; z-index:4; border-left:2px solid white; height:100%;'; + } + + }; + } +}; \ No newline at end of file diff --git a/src/placeMarker.js b/src/placeMarker.js new file mode 100644 index 0000000..723c293 --- /dev/null +++ b/src/placeMarker.js @@ -0,0 +1,71 @@ +'use strict'; + +function placeMarker(width, height, entry, body, url, el) { + //background-image: linear-gradient(90deg, rgba(253, 174, 97, 0.95), rgba(253, 174, 97, 0.95)); + var heat = (entry.responseEnd / loaded), + marker = document.createElement('div'), + padding = 9, + size = 18, + paddingTop, + opacity = 0.925, + align = 'center', + bodyText = ''; + + // adjust size of fonts/padding based on width of overlay + if (width < 170) { + padding = 12; + size = 12; + } else if (width > 400) { + padding = 13; + size = 26; + } + + // check for overlay that matches viewport and assume it's like a background image on body + if ((width === document.documentElement.clientWidth) && (height >= document.documentElement.clientHeight)) { + body = true; + } + + // adjust opacity if it's the body element and position label top right + paddingTop = (height / 2) - padding; + if (!!body) { + opacity = 0.6; + size = 18; + align = 'right'; + paddingTop = 10; + bodyText = 'BODY '; + } + var elem = el.element; + //debugger; + var oldClass = elem.className; + elem.className = oldClass + ' perfmap'; + elem.setAttribute('data-ms', parseInt(entry.responseEnd)); + elem.setAttribute('data-body', (body ? 1 : 0)); + var oldStyle = elem.style.cssText; + var bgImg = ''; + if (!!el.bgImg) { + bgImg = ', ' + el.bgImg; + } else { + //debugger; + bgImg = ', url("' + elem.src + '") '; + + var style = elem.currentStyle || window.getComputedStyle(elem, false); + //console.log(style.getPropertyValue('background-image')); + oldStyle += 'width: ' + (style.getPropertyValue('width') || width + 'px') + '!important;'; + oldStyle += 'height: ' + ( style.getPropertyValue('height') || height + 'px') + '!important;'; + elem.removeAttribute("src"); + } + var bgPosition = 'background-position: 0px 0px'; + if (!!el.position) { + bgPosition += ', ' + el.position; + } + + elem.style.cssText = oldStyle + ' background-image: linear-gradient(' + heatmap(heat).rgba + ', ' + heatmap(heat).rgba + ')' + bgImg + '; ' + bgPosition + '; background-size: contain;'; + //elem.style.cssText = 'position:absolute; transition: 0.5s ease-in-out; box-sizing: border-box; color: #fff; padding-left:10px; padding-right:10px; line-height:14px; font-size: ' + size + 'px; font-weight:800; font-family:"Helvetica Neue",sans-serif; text-align:' + align + '; opacity: ' + opacity + '; background: ' + heatmap(heat).value + '; top: ' + xy.top + 'px; left: ' + xy.left + 'px; width: ' + width + 'px; height:' + height + 'px; padding-top:' + paddingTop + 'px; z-index: 4000;'; + // if (width > 50) { + // if (height > 15) { + // oldStyle = elem.style.cssText; + // elem.style.cssText = oldStyle + ' content: "' + bodyText + parseInt(entry.responseEnd) + 'ms (' + parseInt(entry.duration) + 'ms)";'; + // } + // } + //document.body.appendChild(marker); +} \ No newline at end of file diff --git a/src/startLoading.js b/src/startLoading.js new file mode 100644 index 0000000..948c297 --- /dev/null +++ b/src/startLoading.js @@ -0,0 +1,41 @@ +'use strict'; + +function startLoading() { + + // give visual feedback asap + loading = document.createElement('div'); + loading.id = 'perfmap-loading'; + loading.innerHTML = 'Creating PerfMap'; + loading.style.cssText = 'position:absolute; z-index:6000; left:40%; top:45%; background-color:#000; color:#fff; padding:20px 30px; font-family:"Helvetica Neue",sans-serif; font-size:24px; font-weight:800;border:2px solid white;'; + document.body.appendChild(loading); + + // get full page load time to calculate heatmap max + loaded = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart; + + // backend + var backend = window.performance.timing.responseEnd - window.performance.timing.navigationStart; + var backendLeft = (backend / loaded) * 100; + var paint, firstPaint, firstPaintLeft; + + // first paint in chrome from https://github.com/addyosmani/timing.js + if (window.chrome && window.chrome.loadTimes) { + paint = window.chrome.loadTimes().firstPaintTime * 1000; + firstPaint = paint - (window.chrome.loadTimes().startLoadTime * 1000); + firstPaintLeft = (firstPaint / loaded) * 100; + } + + // remove any exisiting "perfmap" divs on second click + var elements = document.getElementsByClassName('perfmap'); + while (elements.length > 0) { + elements[0].parentNode.removeChild(elements[0]); + } + + // build bottom legend + var perfmap = document.createElement('div'); + perfmap.id = 'perfmap'; + perfmap.style.cssText = 'position: fixed; width:100%; bottom:0; left:0; z-index:5000; height: 25px; color:#fff; font-family:"Helvetica Neue",sans-serif; font-size:14px; font-weight:800; line-height:14px;'; + perfmap.innerHTML = '
Fully Loaded ' + parseInt(loaded) + 'ms
First Paint ' + parseInt(firstPaint) + 'ms
'; + document.body.appendChild(perfmap); + + +} \ No newline at end of file