Remove JS Timeline and Scroll Bottlenecks

This commit is contained in:
Gaël Métais
2019-03-13 19:27:57 +01:00
parent b6e8768767
commit 5e82eea9f1
21 changed files with 1 additions and 3070 deletions
-48
View File
@@ -66,24 +66,6 @@ var policies = {
};
}
},
"DOMaccesses": {
"tool": "jsExecutionTransformer",
"label": "DOM access",
"message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
"isOkThreshold": 200,
"isBadThreshold": 2000,
"isAbnormalThreshold": 4000,
"hasOffenders": false
},
"queriesWithoutResults": {
"tool": "jsExecutionTransformer",
"label": "Queries without result",
"message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
"isOkThreshold": 0,
"isBadThreshold": 150,
"isAbnormalThreshold": 250,
"hasOffenders": false
},
"DOMqueriesAvoidable": {
"tool": "phantomas",
"label": "Duplicated DOM queries",
@@ -147,18 +129,6 @@ var policies = {
};
}
},
"DOMaccessesOnScroll": {
"tool": "jsExecutionTransformer",
"label": "DOM access on scroll",
"message": "<p>This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or debouncing</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 20,
"isAbnormalThreshold": 35,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"jsErrors": {
"tool": "phantomas",
"label": "JavaScript errors",
@@ -348,24 +318,6 @@ var policies = {
"isAbnormalThreshold": 2,
"hasOffenders": true
},
"jQueryCallsOnEmptyObject": {
"tool": "jsExecutionTransformer",
"label": "Calls on empty objects",
"message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"jQueryNotDelegatedEvents": {
"tool": "jsExecutionTransformer",
"label": "Events not delegated",
"message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"cssParsingErrors": {
"tool": "phantomas",
"label": "CSS syntax error",
-17
View File
@@ -32,21 +32,6 @@
"DOMidDuplicated": 1
}
},
"domManipulations": {
"label": "DOM manipulations",
"policies": {
"DOMaccesses": 3,
"queriesWithoutResults": 1,
"DOMqueriesAvoidable": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
}
},
"badJavascript": {
"label": "Bad JavaScript",
"policies": {
@@ -116,8 +101,6 @@
"pageWeight": 3,
"requests": 3,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 2,
"jQuery": 1,
"cssSyntaxError": 1,
-4
View File
@@ -2,7 +2,6 @@ var Q = require('q');
var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var isHttp2 = require('./tools/isHttp2');
@@ -28,9 +27,6 @@ var Runner = function(params) {
.then(function(phantomasResults) {
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data = jsExecutionTransformer.transform(data);
// Compare colors
data = colorDiff.compareAllColors(data);
+1 -1
View File
@@ -10,7 +10,7 @@ var FrontController = function(app) {
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
// Routes templating
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
var routes = ['/', '/about', '/result/:runId', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
routes.forEach(function(route) {
app.get(route, function(req, res) {
-210
View File
@@ -1,210 +0,0 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
var offenders = {};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node, index) {
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if (isABindWithoutEventDelegation(node, contextLength)) {
metrics.jQueryNotDelegatedEvents += contextLength;
node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.queryWithoutResults = true;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.jQueryCallOnEmptyObject = true;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
}
// Mark errors with an error flag
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
node.error = true;
}
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
// Adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
javascriptExecutionTree.data.domComplete = node.data.timestamp;
break;
}
}
// Fix rare bug when domComplete was never triggered
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
}
debug('JS execution transformation complete');
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
}
debug('Scroll execution transformation complete');
} else {
debug('Could not parse scrollExecutionTree');
}
} catch(err) {
throw err;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics,
offenders: offenders
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
};
module.exports = new jsExecutionTransformer();
@@ -1,387 +0,0 @@
/**
* Analyzes DOM queries done via native DOM methods
*/
/* global Element: true, Document: true, Node: true, window: true */
exports.version = '0.10.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
// fake native DOM functions
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context, hasNoResults) {
phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
}
phantomas.spy(Document.prototype, 'getElementById', function(id) {
phantomas.incrMetric('DOMqueriesById');
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
var id = args ? '#' + args[0] : undefined;
querySpy('id', id, 'getElementById', '#document', (result === null));
var moreData = {
resultsNumber : (result === null) ? 0 : 1
};
phantomas.leaveContext(moreData);
});
// selectors by class name
function selectorClassNameSpyBefore(className) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorClassNameAfter(result, args) {
/*jshint validthis: true */
var className = args ? '.' + args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('class', className, 'getElementsByClassName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
// selectors by tag name
function selectorTagNameSpyBefore(tagName) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorTagNameSpyAfter(result, args) {
/*jshint validthis: true */
var tagName = args ? args[0].toLowerCase() : undefined;
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName, 'getElementsByTagName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
// selector queries
function selectorQuerySpy(selector, context) {
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
}
function selectorQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelector',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorQuerySpyAfter(result, args) {
/*jshint validthis: true */
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : result ? 1 : 0
};
phantomas.leaveContext(moreData);
}
function selectorAllQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelectorAll',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorAllQuerySpryAfter(result, args) {
/*jshint validthis: true */
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
// count DOM inserts
function appendChild(child, element, context, appended) {
/*jshint validthis: true */
// ignore appending to the node that's not yet added to DOM tree
if (!element.parentNode) {
return;
}
// don't count elements added to fragments as a DOM inserts (issue #350)
// DocumentFragment > div[0]
if (context.indexOf('DocumentFragment') === 0) {
return;
}
phantomas.incrMetric('DOMinserts');
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
}
function appendChildSpyBefore(child) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'appendChild',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [appended]
},
backtrace: phantomas.getBacktrace()
});
}
function insertBeforeSpyBefore(child, refElement) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
var referent = phantomas.getDOMPath(refElement);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [
appended,
referent
]
},
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
phantomas.enterContext({
type: 'createElement',
callDetails: {
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
phantomas.enterContext({
type: 'createTextNode',
callDetails: {
arguments: [text]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
phantomas.enterContext({
type: 'createDocumentFragment',
callDetails: {
arguments: []
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
var target = phantomas.getDOMPath(element);
phantomas.enterContext({
type: 'getComputedStyle',
callDetails: {
arguments: [target, pseudoElement]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
// report DOM queries that return no results (issue #420)
phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
// ignore DOM queries within DOM fragments (used internally by jQuery)
if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
return;
}
if (hasNoResults === true) {
phantomas.incrMetric('DOMqueriesWithoutResults');
phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
}
});
// count DOM queries by either ID, tag name, class name and selector query
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../util/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName, context) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && (
context.indexOf('html') === 0 ||
context.indexOf('body') === 0 ||
context.indexOf('head') === 0 ||
context.indexOf('#document') === 0
)) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
phantomas.on('report', function() {
DOMqueries.sort().forEach(function(query, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMqueriesDuplicated');
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
}
});
});
};
@@ -1,381 +0,0 @@
/**
* Analyzes jQuery activity
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
/* jshint -W030 */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
var jQueryFunctions = [
// DOM manipulations
'html',
'append',
'appendTo',
'prepend',
'prependTo',
'before',
'insertBefore',
'after',
'insertAfter',
'remove',
'detach',
'empty',
'clone',
'replaceWith',
'replaceAll',
'text',
'wrap',
'wrapAll',
'wrapInner',
'unwrap',
// Style manipulations
'css',
'offset',
'position',
'height',
'innerHeight',
'outerHeight',
'width',
'innerWidth',
'outerWidth',
'scrollLeft',
'scrollTop',
// Animations
'hide',
'show',
'toggle',
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideDown',
'slideUp',
'slideToggle',
// Generic events
'on',
'off',
'live',
'die',
'delegate',
'undelegate',
'one',
'bind',
'unbind',
// More events
'blur',
'change',
'click',
'dblclick',
'error',
'focus',
'focusin',
'focusout',
'hover',
'keydown',
'keypress',
'keyup',
'load',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'resize',
'scroll',
'select',
'submit',
'unload',
// Attributes
'attr',
'prop',
'removeAttr',
'removeProp',
'val',
'hasClass',
'addClass',
'removeClass',
'toggleClass',
];
var jQueryTraversalFunctions = [
'children',
'closest',
'find',
'next',
'nextAll',
'nextUntil',
'offsetParent',
'parent',
'parents',
'parentsUntil',
'prev',
'prevAll',
'prevUntil',
'siblings'
];
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.on('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var oldJQuery;
phantomas.spyGlobalVar('jQuery', function(jQuery) {
var version;
if (!jQuery || !jQuery.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
// Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
// when it's actually only restored via $.noConflict(true) - see comments in #435
if (jQuery.__phantomas === true) {
phantomas.log('jQuery: this instance has already been seen by phantomas');
return;
}
jQuery.__phantomas = true;
// report the version of jQuery
version = jQuery.fn.jquery;
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
arguments: ['version ' + version]
},
backtrace: phantomas.getBacktrace()
});
oldJQuery = version;
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(jQuery.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
phantomas.pushContext({
type: 'jQuery - onDOMReady',
callDetails: {
arguments: [func]
},
backtrace: phantomas.getBacktrace()
});
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(jQuery, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
phantomas.enterContext({
type: 'jQuery - Sizzle call',
callDetails: {
context: {
length: 1,
elements: [phantomas.getDOMPath(context)]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
var data = phantomas.getContextData();
if (data.type === 'jQuery - find' &&
!data.callDetails.context &&
data.callDetails.arguments.length === 1 &&
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
});
if (!jQuery.event) {
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
var path = phantomas.getDOMPath(elem),
type = ev.type || ev;
phantomas.log('Event: triggered "%s" on "%s"', type, path);
phantomas.incrMetric('jQueryEventTriggers');
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
});
}
// jQuery events bound to window' onLoad event (#451)
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
if ((eventName === 'load') && (this[0] === window)) {
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
}
});
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
phantomas.spy(jQuery.fn, functionName, function(args) {
// Clean args
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Array) {
arg = '[Array]';
}
if (arg instanceof Object) {
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
} else if (arg instanceof HTMLElement) {
arg = phantomas.getDOMPath(arg) || 'unknown';
} else if (typeof arg === 'function') {
arg = '(function)';
} else {
try {
for (var key in arg) {
if (typeof arg[key] === 'function') {
arg[key] = '(function)';
}
}
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
}
}
}
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
arg = arg.substring(0, 200) + '...';
}
if (typeof arg === 'function') {
arg = '(function)';
}
if (arg === true) {
arg = 'true';
}
if (arg === false) {
arg = 'false';
}
if (arg === null) {
arg = 'null';
}
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
arg = 'undefined';
}
args[index] = arg;
});
var elements = [];
for (var i = 0 ; i < this.length ; i++) {
elements.push(phantomas.getDOMPath(this[i]));
}
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
elements: elements
},
arguments: args
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
} else {
phantomas.leaveContext();
}
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
});
});
})(window.__phantomas);
}, jQueryFunctions, jQueryTraversalFunctions);
});
phantomas.on('jQueryLoaded', function(version) {
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
// report multiple jQuery "instances" (issue #435)
phantomas.incrMetric('jQueryVersionsLoaded');
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
});
};
@@ -1,69 +0,0 @@
/**
* Reports the use of functions known to be serious performance bottlenecks in JS
*
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
*
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
// spy calls to eval only when requested (issue #467)
var spyEval = phantomas.getParam('spy-eval') === true;
if (!spyEval) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.on('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
if (spyEval) {
phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});
}
// spy calls to setTimeout / setInterval with string passed instead of a function
phantomas.spy(window, 'setTimeout', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
phantomas.spy(window, 'setInterval', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
// spy document.write(ln)
phantomas.spy(document, 'write', function(arg) {
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
phantomas.spy(document, 'writeln', function(arg) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
}, spyEval);
});
};
@@ -1,31 +0,0 @@
/**
* Saves the javascript interractions with the DOM
*
* Run phantomas with --js-execution-tree option to use this module
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree');
// save data
phantomas.on('report', function() {
phantomas.log('JS execution tree: Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
if (fullTree === null) {
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
return;
}
phantomas.setMetric('javascriptExecutionTree', true, true);
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
})(window.__phantomas);});
});
};
@@ -1,69 +0,0 @@
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('scrollExecutionTree');
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('scroll', false, false, null);
function triggerScrollEvent() {
phantomas.resetTree();
try {
// Chrome triggers them in this order:
// 1. document
phantomas.pushContext({
type: 'documentScroll'
});
document.dispatchEvent(evt);
// 2. window
phantomas.pushContext({
type: 'windowScroll'
});
window.dispatchEvent(evt);
// No need to call window.onscroll(), it's called by the scroll event on window
} catch(e) {
phantomas.log('ScrollListener error: %s', e);
}
}
var firstScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a first scroll event...');
triggerScrollEvent();
// Ignore the first scroll event and only save the second one,
// because we want to detect un-throttled things, throttled ones are ok.
var secondScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
triggerScrollEvent();
var fullTree = phantomas.readFullTree();
if (fullTree !== null) {
phantomas.setMetric('scrollExecutionTree', true, true);
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
} else {
phantomas.log('Error: scrollExecutionTree could not be extracted');
}
phantomas.log('ScrollListener: end of scroll triggering');
})(window.__phantomas);
});
});
};
@@ -1,150 +0,0 @@
/**
* Measure when the page reaches certain states
*
* @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
* @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// times below are calculated relative to performance.timing.responseEnd (#117)
phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM
phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning
// backend vs frontend time
phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%]
phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
// measure dom... metrics from the moment HTML response was fully received
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
});
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
phantomas.spyEnabled(false, 'installing window.performance metrics');
phantomas.currentStep = 'domCreation';
// extend window.performance
// "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
if (typeof window.performance === 'undefined') {
window.performance = {
timing: {
responseEnd: responseEndTime
}
};
phantomas.log('Performance timing: emulating window.performance');
}
else {
phantomas.log('Performance timing: using native window.performance');
}
// onDOMReady
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
// use NavigationTiming if possible
var time = window.performance.timing.domContentLoadedEventEnd ?
(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
:
(Date.now() - responseEndTime);
phantomas.currentStep = 'domContentLoadedEnd';
phantomas.setMetric('domContentLoadedEnd', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoadedEnd'
});
}, 0);
var time = Date.now() - responseEndTime;
phantomas.currentStep = 'domContentLoaded';
phantomas.setMetric('domContentLoaded', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoaded'
});
});
// emulate Navigation Timing
document.addEventListener('readystatechange', function() {
var readyState = document.readyState,
responseEndTime = window.performance.timing.responseEnd,
time = Date.now() - responseEndTime,
metricName;
// @see http://www.w3.org/TR/html5/dom.html#documentreadystate
switch(readyState) {
// the browser has finished parsing all of the HTML and DOM construction is complete
case 'interactive':
metricName = 'domInteractive';
break;
// the processing is complete and all of the resources on the page have finished downloading
case 'complete':
metricName = 'domComplete';
phantomas.log('Performance timing: %j', window.performance.timing);
break;
default:
phantomas.log('Performance timing: unhandled "%s" state!', readyState);
return;
}
phantomas.currentStep = metricName;
phantomas.setMetric(metricName, time, true);
phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
phantomas.pushContext({
type: metricName
});
});
phantomas.spyEnabled(true);
})(window.__phantomas);
}, responseEndTime);
});
/**
* Emit metrics with backend vs frontend time
*
* Performance Golden Rule:
* "80-90% of the end-user response time is spent on the frontend. Start there."
*
* @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
*/
phantomas.on('report', function() {
// The “backend” time is the time it takes the server to get the first byte back to the client.
// The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
totalTime = backendTime + frontendTime,
backendTimePercentage;
if (totalTime === 0) {
return;
}
backendTimePercentage = Math.round(backendTime / totalTime * 100);
phantomas.setMetric('timeBackend', backendTimePercentage);
phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
});
};