New rules: eventsScrollBound & DOMaccessesOnScroll

This commit is contained in:
Gaël Métais
2015-05-06 22:45:03 +02:00
parent b0c6fe9474
commit f7da653e87
15 changed files with 344 additions and 46 deletions
+51
View File
@@ -66,6 +66,15 @@ var policies = {
};
}
},
"DOMaccesses": {
"tool": "jsExecutionTransformer",
"label": "DOM access",
"message": "<p>TODO</p><p>TODO</p>",
"isOkThreshold": 50,
"isBadThreshold": 2000,
"isAbnormalThreshold": 3000,
"hasOffenders": false
},
"DOMinserts": {
"tool": "phantomas",
"label": "DOM inserts",
@@ -195,6 +204,48 @@ var policies = {
};
}
},
"eventsScrollBound": {
"tool": "phantomas",
"label": "Scroll events bound",
"message": "<p>Number of 'scroll' event listeners binded to 'window' or 'document'.</p><p>Asking too much work to the browser on scroll hurts the smoothness of the scroll. Merging all your event listeners into an unique listener can help you factorize their code and reduce their footprint on scroll.</p>",
"isOkThreshold": 1,
"isBadThreshold": 7,
"isAbnormalThreshold": 12,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^bound by (.*)$/.exec(offender);
if (!parts) {
debug('eventsScrollBound offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
var backtraceArray = offendersHelpers.backtraceToArray(parts[1]);
return {
backtrace: backtraceArray || []
};
})
};
}
},
"DOMaccessesOnScroll": {
"tool": "jsExecutionTransformer",
"label": "DOM access on scroll",
"message": "<p>This rule counts the number of DOM-accessing functions calls, 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 deboucing</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 10,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"jsErrors": {
"tool": "phantomas",
"label": "JavaScript errors",
+9
View File
@@ -12,6 +12,7 @@
"domManipulations": {
"label": "DOM manipulations",
"policies": {
"DOMaccesses": 3,
"DOMinserts": 2,
"DOMqueries": 1,
"DOMqueriesWithoutResults": 2,
@@ -19,6 +20,13 @@
"eventsBound": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
}
},
"badJavascript": {
"label": "Bad JavaScript",
"policies": {
@@ -99,6 +107,7 @@
"globalScore": {
"domComplexity": 1,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 1,
"jQueryVersion": 1,
"cssSyntaxError": 1,
+1 -1
View File
@@ -115,7 +115,7 @@ var OffendersHelpers = function() {
var parts = null;
for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) {
var obj = {
+2
View File
@@ -42,6 +42,8 @@ var Runner = function(params) {
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
return data;
+1
View File
@@ -125,6 +125,7 @@ var ApiController = function(app) {
// Empty javascriptExecutionTree if not needed
if (!run.params.jsTimeline) {
data.javascriptExecutionTree = {};
data.scrollExecutionTree = {};
}
// Remove tools results if not needed
+68 -21
View File
@@ -6,18 +6,19 @@ var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var scrollExecutionTree = {};
var metrics = {
domManipulations: 0,
DOMaccesses: 0,
queriesWithoutResults: 0,
jQueryCalls: 0,
jQueryCallsOnEmptyObject: 0
jQueryCallsOnEmptyObject: 0,
DOMaccessesOnScroll: 0
};
debug('Starting JS execution transformation');
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
@@ -57,33 +58,46 @@ var jsExecutionTransformer = function() {
break;
}
// Change the list of dom paths into a tree
treeRecursiveParser(node, 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);
}
// Transform domPaths into objects
changeListOfDomPaths(node);
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]);
}
});
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
}
debug('JS execution transformation complete');
debug('Starting scroll execution transformation');
scrollExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (scrollExecutionTree.children) {
scrollExecutionTree.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');
} catch(err) {
throw err;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics
metrics: metrics,
offenders: {
DOMaccessesOnScroll: scrollExecutionTree
}
};
return data;
@@ -97,6 +111,39 @@ var jsExecutionTransformer = function() {
}
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;
}
};
module.exports = new jsExecutionTransformer();
@@ -104,10 +104,11 @@ exports.module = function(phantomas) {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
var depth = 0;
if (deepAnalysis) {
phantomas.log('Entering deep Javascript analysis mode');
}
var depth = 0;
// Add a child but don't enter his context
function pushContext(data) {
@@ -176,6 +177,13 @@ exports.module = function(phantomas) {
return root;
}
// Empty the tree
function resetTree() {
root = new ContextTreeNode(null, {type: 'main'});
currentContext = root;
depth = 0;
}
function ContextTreeNode(parent, data) {
@@ -196,6 +204,7 @@ exports.module = function(phantomas) {
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
phantomas.resetTree = resetTree;
})();
@@ -9,11 +9,11 @@ exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
phantomas.setMetric('javascriptExecutionTree');
// save data
phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON');
phantomas.log('JS execution tree: Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
@@ -0,0 +1,75 @@
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);
// 3. onscroll()
if (window.onscroll) {
phantomas.pushContext({
type: 'window.onscroll'
});
window.onscroll();
}
} 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);
});
});
};