diff --git a/lib/core/constants.js b/lib/core/constants.js index 3880c0a16e..ecd01c8aba 100644 --- a/lib/core/constants.js +++ b/lib/core/constants.js @@ -27,7 +27,11 @@ const definitions = [ const constants = { helpUrlBase: 'https://dequeuniversity.com/rules/', + // Size of a grid square in pixels gridSize: 200, + // At a certain point, looping over an array of elements and using .match on them + // is slower than just running querySelectorAll again. + selectorSimilarFilterLimit: 700, results: [], resultGroups: [], resultGroupMap: {}, diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 7824080821..388aeff982 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -59,8 +59,19 @@ export default function runRules(context, options, resolve, reject) { // after should only run once, so ensure we are in the top level window if (context.initiator) { + if (options.performanceTimer) { + performanceTimer.mark('auditAfterStart'); + } results = audit.after(results, options); - + if (options.performanceTimer) { + performanceTimer.mark('auditAfterEnd'); + performanceTimer.measure( + 'audit.after', + 'auditAfterStart', + 'auditAfterEnd' + ); + performanceTimer.logMeasures('audit.after'); + } results.forEach(publishMetaData); results = results.map(finalizeRuleResult); } diff --git a/lib/core/public/run.js b/lib/core/public/run.js index 74983c7e86..c891784b69 100644 --- a/lib/core/public/run.js +++ b/lib/core/public/run.js @@ -2,6 +2,7 @@ import { getReporter } from './reporter'; import normalizeRunParams from './run/normalize-run-params'; import { setupGlobals } from './run/globals-setup'; import { assert } from '../utils'; +import performanceTimer from '../utils/performance-timer'; const noop = () => {}; @@ -33,11 +34,17 @@ export default function run(...args) { axe._running = true; if (options.performanceTimer) { - axe.utils.performanceTimer.start(); + performanceTimer.start(); } function handleRunRules(rawResults, teardown) { const respond = results => { + if (options.performanceTimer) { + performanceTimer.mark('reporterEnd'); + performanceTimer.measure('reporter', 'reporterStart', 'reporterEnd'); + performanceTimer.logMeasures('reporter'); + performanceTimer.end(); + } axe._running = false; teardown(); try { @@ -56,11 +63,10 @@ export default function run(...args) { } }; - if (options.performanceTimer) { - axe.utils.performanceTimer.end(); - } - try { + if (options.performanceTimer) { + performanceTimer.mark('reporterStart'); + } createReport(rawResults, options, respond, wrappedReject); } catch (err) { wrappedReject(err); @@ -69,7 +75,7 @@ export default function run(...args) { function errorRunRules(err) { if (options.performanceTimer) { - axe.utils.performanceTimer.end(); + performanceTimer.end(); } axe._running = false; callback(err); diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index d37354df55..16e156f5fa 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -4,6 +4,8 @@ import getNodeAttributes from './get-node-attributes'; import matchesSelector from './element-matches'; import isXHTML from './is-xhtml'; import getShadowSelector from './get-shadow-selector'; +import memoize from './memoize'; +import constants from '../../core/constants'; const ignoredAttributes = [ 'class', @@ -25,6 +27,7 @@ const ignoredAttributes = [ 'aria-valuenow', 'xmlns' ]; + const MAXATTRIBUTELENGTH = 31; const attrCharsRegex = /([\\"])/g; const newlineChars = /(\r\n|\r|\n)/g; @@ -346,7 +349,6 @@ function getThreeLeastCommonFeatures(elm, selectorData) { * @param {RootNode} doc The root node of the document or document fragment * @returns {String} The selector */ - function generateSelector(elm, options, doc) { /*eslint no-loop-func:0*/ // TODO: es-modules_selectorData @@ -373,8 +375,9 @@ function generateSelector(elm, options, doc) { } else { selector = features; } - if (!similar) { - similar = Array.from(doc.querySelectorAll(selector)); + // If there are too many similar element running QSA again is faster + if (!similar || similar.length > constants.selectorSimilarFilterLimit) { + similar = findSimilar(doc, selector); } else { similar = similar.filter(item => { return matchesSelector(item, selector); @@ -398,6 +401,16 @@ function generateSelector(elm, options, doc) { * @param {Object} optional options * @returns {String|Array} Unique CSS selector for the node */ -export default function getSelector(elm, options) { +function getSelector(elm, options) { return getShadowSelector(generateSelector, elm, options); } + +// Axe can call getSelector more than once for the same element because +// the same element can end up on multiple DqElements. +export default memoize(getSelector); + +// Similar elements create similar selectors. If there are lots of similar elements on the page, +// axe ends up needing to run that same selector many times. We can memoize for a huge perf boost. +const findSimilar = memoize((doc, selector) => + Array.from(doc.querySelectorAll(selector)) +); diff --git a/test/core/constants.js b/test/core/constants.js index 6b66637723..23f05a5333 100644 --- a/test/core/constants.js +++ b/test/core/constants.js @@ -32,4 +32,12 @@ describe('axe.constants', function () { it('should have groups for results', function () { assert.equal(axe.constants.FAIL_GROUP, 'violations'); }); + + it('should have a gridSize', function () { + assert.equal(axe.constants.gridSize, 200); + }); + + it('should have a selectorSimilarFilterLimit', function () { + assert.equal(axe.constants.selectorSimilarFilterLimit, 700); + }); });