Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(performance): significantly improve the performance of the dom.fi… #699

Merged
merged 3 commits into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also add a note here on turning up the iframe timeout option with very large iframes.


#### 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') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in assuming that any virtual node with a truthy shadowId is somewhere inside a shadow tree, either because it is a shadow element or because it's slotted somewhere? Please add a comment to clear that up.

// 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;
};
8 changes: 4 additions & 4 deletions lib/commons/text/accessible-text-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ var phrasingElements = ['A', 'EM', 'STRONG', 'SMALL', 'MARK', 'ABBR', 'DFN', 'I'
* @param {HTMLElement} element The HTMLElement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs look out-of-date.

* @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 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
11 changes: 8 additions & 3 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ describe('color-contrast', function () {
'use strict';

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;

var checkContext = {
_relatedNodes: [],
Expand All @@ -18,6 +19,7 @@ describe('color-contrast', function () {
fixture.innerHTML = '';
checkContext._relatedNodes = [];
checkContext._data = null;
axe._tree = undefined;
});

it('should return the proper values stored in data', function () {
Expand Down Expand Up @@ -206,13 +208,14 @@ describe('color-contrast', function () {
});

it('should return true when a label wraps a text input', function () {
fixture.innerHTML = '<label id="target">' +
'My text <input type="text"></label>';
fixtureSetup('<label id="target">' +
'My text <input type="text"></label>');
var target = fixture.querySelector('#target');
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], target);
if (window.PHANTOMJS) {
assert.ok('PhantomJS is a liar');
} else {
var result = checks['color-contrast'].evaluate.call(checkContext, target);
var result = checks['color-contrast'].evaluate.call(checkContext, target, {}, virtualNode);
assert.isTrue(result);
}
});
Expand All @@ -228,13 +231,15 @@ describe('color-contrast', function () {
it('should return true when there is sufficient contrast based on thead', function () {
fixture.innerHTML = '<table><thead style="background: #d00d2c"><tr><th id="target" style="color: #fff; padding: .5em">Col 1</th></tr></thead></table>';
var target = fixture.querySelector('#target');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should return true when there is sufficient contrast based on tbody', function () {
fixture.innerHTML = '<table><tbody style="background: #d00d2c"><tr><td id="target" style="color: #fff; padding: .5em">Col 1</td></tr></tbody></table>';
var target = fixture.querySelector('#target');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
});
Expand Down
Loading