Remove JS Timeline and Scroll Bottlenecks
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user