Remove JS Timeline and Scroll Bottlenecks
This commit is contained in:
@@ -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