Skip to content

Commit

Permalink
Merge pull request #699 from dequelabs/findup-perf
Browse files Browse the repository at this point in the history
fix(performance): significantly improve the performance of the dom.fi…
  • Loading branch information
dylanb authored Jan 30, 2018
2 parents b553e70 + 9320ca6 commit 752d16c
Show file tree
Hide file tree
Showing 22 changed files with 382 additions and 126 deletions.
37 changes: 36 additions & 1 deletion doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
1. [Virtual DOM Utilities](#virtual-dom-utilities)
1. [API Name: axe.utils.querySelectorAll](#api-name-axeutilsqueryselectorall)
1. [Common Functions](#common-functions)
1. [Section 3: Example Reference](#section-5-example-reference)
1. [Section 3: Example Reference](#section-3-example-reference)
1. [Section 4: Performance](#section-4-performance)

## Section 1: Introduction

Expand Down Expand Up @@ -716,3 +717,37 @@ Either the matching HTMLElement or `null` if there was no match.
## Section 3: Example Reference

This package contains examples for [jasmine](examples/jasmine), [mocha](examples/mocha), [phantomjs](examples/phantomjs), [qunit](examples/qunit), [selenium using javascript](examples/selenium), and [generating HTML from the violations array](examples/html-handlebars.md). Each of these examples is in the [doc/examples](examples) folder. In each folder, there is a README.md file which contains specific information about each example.

## Section 4: Performance

Axe-core performs very well in general and if you are analyzing average complexity pages with the default settings, you should not need to worry about performance at all. There are some scenarios that can cause performance issues. This is the list of known issues and what you can do to mitigate and/or avoid them.

### Very large pages

Certain rules (like the color-contrast rule) look at almost every element on a page and some of these rules also perform somewhat expensive operations on these elements including looking up the hierarchy, looking at overlapping elements, calculating the computed styles etc. It also calculates a unique selector for each element in the results and also de-duplicates elements so that you do not get duplicate items in your results.

If your page is very large (in terms of the number of Elements on the page) i.e. >50K elements on the page, then you will see analysis times that run over 10s on a relatively decent CPU.

#### Use resultTypes

An approach you can take to reducing the time is use the `resultTypes` option. By calling `axe.run` with the following options, axe-core will only return the full details of the `violations` array and will only return one instance of each of the `inapplicable`, `incomplete` and `pass` array for each rule that has at least one of those entries. This will reduce the amount of computation that axe-core does for the unique selectors.

```
{
resultTypes: ['violations']
}
```

### Multiple include regions

If you tell axe-core to only analyze a section of the page and you have multiple selectors for pages to include, then axe-core must select within each region (which could be overlapping) and de-duplicate the matching elements. This is expensive and becomes more expensive, the more regions you supply and the larger the page.

#### Analyze each region separately

If you find yourself doing this with regions that contain a lot of elements, it might be faster to analyze each region separately and then merge the results outside of axe-core. Especially if you know that the regions are not overlapping, you can easily just concat the results.

### Other strategies

#### Targeted color-contrast analysis

If you are analyzing multiple pages on a single Web site or application, chances are these pages all contain the same styles. It is therefore not adding any additional information to your analysis to analyze every page for color-contrast. Choose a small number of pages that represent the totality of you styles and analyze these with color-contrast and analyze all others without it.
12 changes: 6 additions & 6 deletions lib/checks/aria/required-parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ function getSelector(role) {
return impliedNative.concat('[role="' + role + '"]').join(',');
}

function getMissingContext(element, requiredContext, includeElement) {
function getMissingContext(virtualNode, requiredContext, includeElement) {
var index, length,
role = element.getAttribute('role'),
role = virtualNode.actualNode.getAttribute('role'),
missing = [];

if (!requiredContext) {
Expand All @@ -15,10 +15,10 @@ function getMissingContext(element, requiredContext, includeElement) {
if (!requiredContext) { return null; }

for (index = 0, length = requiredContext.length; index < length; index++) {
if (includeElement && axe.utils.matchesSelector(element, getSelector(requiredContext[index]))) {
if (includeElement && axe.utils.matchesSelector(virtualNode.actualNode, getSelector(requiredContext[index]))) {
return null;
}
if (axe.commons.dom.findUp(element, getSelector(requiredContext[index]))) {
if (axe.commons.dom.findUpVirtual(virtualNode, getSelector(requiredContext[index]))) {
//if one matches, it passes
return null;
} else {
Expand Down Expand Up @@ -46,15 +46,15 @@ function getAriaOwners(element) {
return owners.length ? owners : null;
}

var missingParents = getMissingContext(node);
var missingParents = getMissingContext(virtualNode);

if (!missingParents) { return true; }

var owners = getAriaOwners(node);

if (owners) {
for (var i = 0, l = owners.length; i < l; i++) {
missingParents = getMissingContext(owners[i], missingParents, true);
missingParents = getMissingContext(axe.utils.getNodeFromTree(axe._tree[0], owners[i]), missingParents, true);
if (!missingParents) { return true; }
}
}
Expand Down
18 changes: 9 additions & 9 deletions lib/checks/forms/fieldset.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,22 @@ function spliceCurrentNode(nodes, current) {
});
}

function runCheck(element) {
const name = axe.commons.utils.escapeSelector(node.name);
const root = axe.commons.dom.getRootNode(node);
function runCheck(virtualNode) {
const name = axe.commons.utils.escapeSelector(virtualNode.actualNode.name);
const root = axe.commons.dom.getRootNode(virtualNode.actualNode);
const matchingNodes = root.querySelectorAll('input[type="' +
axe.commons.utils.escapeSelector(node.type) + '"][name="' + name + '"]');
axe.commons.utils.escapeSelector(virtualNode.actualNode.type) + '"][name="' + name + '"]');

if (matchingNodes.length < 2) {
return true;
}
const fieldset = axe.commons.dom.findUp(element, 'fieldset');
const group = axe.commons.dom.findUp(element, '[role="group"]' +
(node.type === 'radio' ? ',[role="radiogroup"]' : ''));
const fieldset = axe.commons.dom.findUpVirtual(virtualNode, 'fieldset');
const group = axe.commons.dom.findUpVirtual(virtualNode, '[role="group"]' +
(virtualNode.actualNode.type === 'radio' ? ',[role="radiogroup"]' : ''));

if (!group && !fieldset) {
failureCode = 'no-group';
self.relatedNodes(spliceCurrentNode(matchingNodes, element));
self.relatedNodes(spliceCurrentNode(matchingNodes, virtualNode.actualNode));
return false;

} else if (fieldset) {
Expand All @@ -86,7 +86,7 @@ var data = {
type: node.getAttribute('type')
};

var result = runCheck(node);
var result = runCheck(virtualNode);
if (!result) {
data.failureCode = failureCode;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/label/implicit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

var label = axe.commons.dom.findUp(node, 'label');
var label = axe.commons.dom.findUpVirtual(virtualNode, 'label');
if (label) {
return !!axe.commons.text.accessibleText(label);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/navigation/p-as-heading.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ if (!nextStyle || !isHeaderStyle(currStyle, nextStyle, margins)) {
return true;
}

let blockquote = axe.commons.dom.findUp(node, 'blockquote');
let blockquote = axe.commons.dom.findUpVirtual(virtualNode, 'blockquote');
if (blockquote && blockquote.nodeName.toUpperCase() === 'BLOCKQUOTE') {
return undefined;
}
Expand Down
48 changes: 34 additions & 14 deletions lib/commons/dom/find-up.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
/* global dom, axe */
/**
* recusively walk up the DOM, checking for a node which matches a selector
* Find the virtual node and call dom.fundUpVirtual
*
* **WARNING:** this should be used sparingly, as it's not even close to being performant
* @method findUp
* @memberof axe.commons.dom
* @instance
* @param {HTMLElement|String} element The starting HTMLElement
* @param {HTMLElement} element The starting HTMLElement
* @param {String} target The selector for the HTMLElement
* @return {HTMLElement|null} Either the matching HTMLElement or `null` if there was no match
*/
dom.findUp = function (element, target) {
let doc, matches,
parent = element;
return dom.findUpVirtual(axe.utils.getNodeFromTree(axe._tree[0], element), target);
};

/**
* recusively walk up the DOM, checking for a node which matches a selector
*
* **WARNING:** this should be used sparingly, as it's not even close to being performant
* @method findUpVirtual
* @memberof axe.commons.dom
* @instance
* @param {VirtualNode} element The starting virtualNode
* @param {String} target The selector for the HTMLElement
* @return {HTMLElement|null} Either the matching HTMLElement or `null` if there was no match
*/
dom.findUpVirtual = function (element, target) {
let parent;

parent = element.actualNode;
// virtualNode will have a shadowId if the element lives inside a shadow DOM or is
// slotted into a shadow DOM
if (!element.shadowId && typeof element.actualNode.closest === 'function') {
// non-shadow DOM elements
let match = element.actualNode.closest(target);
if (match) {
return match;
}
return null;
}
// handle shadow DOM elements and older browsers
do {// recursively walk up the DOM, checking each parent node
parent = (parent.assignedSlot ? parent.assignedSlot : parent.parentNode);
if (parent && parent.nodeType === 11) {
matches = null;
parent = parent.host;
}
if (!matches) {
doc = axe.commons.dom.getRootNode(parent);
matches = doc.querySelectorAll(target);
matches = axe.utils.toArray(matches);
if (doc === document && !matches.length) {
return null;
}
}
} while (parent && !matches.includes(parent));
} while (parent && !axe.utils.matchesSelector(parent, target) && parent !== document.documentElement);

if (!axe.utils.matchesSelector(parent, target)) {
return null;
}
return parent;
};
16 changes: 8 additions & 8 deletions lib/commons/text/accessible-text-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ var phrasingElements = ['A', 'EM', 'STRONG', 'SMALL', 'MARK', 'ABBR', 'DFN', 'I'
/**
* Find a non-ARIA label for an element
* @private
* @param {HTMLElement} element The HTMLElement
* @param {VirtualNode} element The VirtualNode instance whose label we are seeking
* @return {HTMLElement} The label element, or null if none is found
*/
function findLabel({ actualNode }) {
function findLabel(virtualNode) {
let label;
if (actualNode.id) {
if (virtualNode.actualNode.id) {
label = dom.findElmsInContext({
elm: 'label', attr: 'for', value: actualNode.id, context: actualNode
elm: 'label', attr: 'for', value: virtualNode.actualNode.id, context: virtualNode.actualNode
})[0];
} else {
label = dom.findUp(actualNode, 'label');
label = dom.findUpVirtual(virtualNode, 'label');
}
return axe.utils.getNodeFromTree(axe._tree[0], label);
}
Expand All @@ -52,7 +52,7 @@ function shouldNeverCheckSubtree({ actualNode }) {
/**
* Calculate value of a form element when treated as a value
* @private
* @param {HTMLElement} element The HTMLElement
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @return {string} The calculated value
*/
function formValueText({ actualNode }) {
Expand Down Expand Up @@ -103,7 +103,7 @@ function checkDescendant({ actualNode }, nodeName) {
/**
* Determine whether an element can be an embedded control
* @private
* @param {HTMLElement} element The HTMLElement
* @param {VirtualNode} element The VirtualNode instance of the element
* @return {boolean} True if embedded control
*/
function isEmbeddedControl(elm) {
Expand Down Expand Up @@ -273,7 +273,7 @@ text.accessibleTextVirtual = function accessibleTextVirtual(element, inLabelledB
* Determine the accessible text of an element, using logic from ARIA:
* http://www.w3.org/TR/accname-aam-1.1/#mapping_additional_nd_name
*
* @param {HTMLElement} element The HTMLElement
* @param {VirtualNode} element The VirtualNode instance of the HTMLElement
* @param {Boolean} inLabelledByContext True when in the context of resolving a labelledBy
* @param {Boolean} inControlContext True when in the context of textifying a widget
* @return {string}
Expand Down
2 changes: 1 addition & 1 deletion lib/commons/text/label-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ text.labelVirtual = function (node) {
}
}

ref = dom.findUp(node.actualNode, 'label');
ref = dom.findUpVirtual(node, 'label');
candidate = ref && text.visible(ref, true);
if (candidate) {
return candidate;
Expand Down
27 changes: 21 additions & 6 deletions lib/core/utils/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,29 @@ function isNodeInContext(node, context) {
function pushNode(result, nodes, context) {
'use strict';

var temp;
var curried = (function (context) {
return function (node) {
return isNodeInContext(node, context);
};
})(context);
nodes = nodes.filter(curried);

if (result.length === 0) {
return nodes;
}
if (result.length < nodes.length) {
// switch so the comparison is shortest
temp = result;
result = nodes;
nodes = temp;
}
for (var i = 0, l = nodes.length; i < l; i++) {
//jshint loopfunc:true
if (!result.find(function (item) {
return item.actualNode === nodes[i].actualNode;
}) && isNodeInContext(nodes[i], context)) {
if (!result.includes(nodes[i])) {
result.push(nodes[i]);
}
}
return result;
}

/**
Expand All @@ -72,9 +87,9 @@ axe.utils.select = function select(selector, context) {
candidate = context.include[i];
if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
axe.utils.matchesSelector(candidate.actualNode, selector)) {
pushNode(result, [candidate], context);
result = pushNode(result, [candidate], context);
}
pushNode(result, axe.utils.querySelectorAll(candidate, selector), context);
result = pushNode(result, axe.utils.querySelectorAll(candidate, selector), context);
}

return result.sort(axe.utils.nodeSorter);
Expand Down
11 changes: 7 additions & 4 deletions lib/rules/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
var nodeName = node.nodeName.toUpperCase(),
nodeType = node.type;

if (node.getAttribute('aria-disabled') === 'true' || axe.commons.dom.findUp(node, '[aria-disabled="true"]')) {
if (node.getAttribute('aria-disabled') === 'true' ||
axe.commons.dom.findUpVirtual(virtualNode, '[aria-disabled="true"]')) {
return false;
}

Expand All @@ -23,16 +24,18 @@ if (nodeName === 'OPTION') {
return false;
}

if (nodeName === 'BUTTON' && node.disabled || axe.commons.dom.findUp(node, 'button[disabled]')) {
if (nodeName === 'BUTTON' && node.disabled ||
axe.commons.dom.findUpVirtual(virtualNode, 'button[disabled]')) {
return false;
}

if (nodeName === 'FIELDSET' && node.disabled || axe.commons.dom.findUp(node, 'fieldset[disabled]')) {
if (nodeName === 'FIELDSET' && node.disabled ||
axe.commons.dom.findUpVirtual(virtualNode, 'fieldset[disabled]')) {
return false;
}

// check if the element is a label or label descendant for a disabled control
var nodeParentLabel = axe.commons.dom.findUp(node, 'label');
var nodeParentLabel = axe.commons.dom.findUpVirtual(virtualNode, 'label');
if (nodeName === 'LABEL' || nodeParentLabel) {
var relevantNode = node;
var relevantVirtualNode = virtualNode;
Expand Down
1 change: 1 addition & 0 deletions test/checks/aria/required-parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('aria-required-parent', function () {
afterEach(function () {
fixture.innerHTML = '';
checkContext._data = null;
axe._tree = undefined;
});

it('should detect missing required parent', function () {
Expand Down
Loading

0 comments on commit 752d16c

Please sign in to comment.