Skip to content

Commit c9cd122

Browse files
committed
fix(perf): memoize axe.utils.select
1 parent 3274919 commit c9cd122

File tree

4 files changed

+90
-11
lines changed

4 files changed

+90
-11
lines changed

lib/core/base/audit.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Audit.prototype.run = function (context, options, resolve, reject) {
145145
this.validateOptions(options);
146146

147147
axe._tree = axe.utils.getFlattenedTree(document.documentElement); //cache the flattened tree
148+
axe._selectCache = [];
148149
var q = axe.utils.queue();
149150
this.rules.forEach(function (rule) {
150151
if (axe.utils.ruleShouldRun(rule, context, options)) {
@@ -180,6 +181,7 @@ Audit.prototype.run = function (context, options, resolve, reject) {
180181
});
181182
q.then(function (results) {
182183
axe._tree = undefined; // empty the tree
184+
axe._selectCache = undefined; // remove the cache
183185
resolve(results.filter(function (result) { return !!result; }));
184186
}).catch(reject);
185187
};

lib/core/utils/select.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,10 @@ function isNodeInContext(node, context) {
4545
* @param {Array} nodes The list of nodes to push
4646
* @param {Object} context The "resolved" context object, @see resolveContext
4747
*/
48-
function pushNode(result, nodes, context) {
48+
function pushNode(result, nodes) {
4949
'use strict';
5050

5151
var temp;
52-
var curried = (function (context) {
53-
return function (node) {
54-
return isNodeInContext(node, context);
55-
};
56-
})(context);
57-
nodes = nodes.filter(curried);
5852

5953
if (result.length === 0) {
6054
return nodes;
@@ -96,23 +90,45 @@ function hasOverlappingIncludes(includes) {
9690
* @return {Array} Matching virtual DOM nodes sorted by DOM order
9791
*/
9892
axe.utils.select = function select(selector, context) {
93+
//jshint maxstatements:20
9994
'use strict';
10095

10196
var result = [], candidate;
10297
if (!Array.isArray(context.include)) {
10398
context.include = Array.from(context.include);
10499
}
105100
context.include.sort(axe.utils.nodeSorter); // ensure that the order of the include nodes is document order
106-
for (var i = 0, l = context.include.length; i < l; i++) {
101+
if (axe._selectCache) { // if used outside of run, it will still work
102+
for (var j = 0, l = axe._selectCache.length; j < l; j++) {
103+
// First see whether the item exists in the cache
104+
let item = axe._selectCache[j];
105+
if (item.selector === selector) {
106+
return item.result;
107+
}
108+
}
109+
}
110+
var curried = (function (context) {
111+
return function (node) {
112+
return isNodeInContext(node, context);
113+
};
114+
})(context);
115+
for (var i = 0; i < context.include.length; i++) {
107116
candidate = context.include[i];
108117
if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
109-
axe.utils.matchesSelector(candidate.actualNode, selector)) {
110-
result = pushNode(result, [candidate], context);
118+
axe.utils.matchesSelector(candidate.actualNode, selector) &&
119+
curried(candidate)) {
120+
result = pushNode(result, [candidate]);
111121
}
112-
result = pushNode(result, axe.utils.querySelectorAll(candidate, selector), context);
122+
result = pushNode(result, axe.utils.querySelectorAllFilter(candidate, selector, curried));
113123
}
114124
if (context.include.length > 1 && hasOverlappingIncludes(context.include)) {
115125
result.sort(axe.utils.nodeSorter);
116126
}
127+
if (axe._selectCache) {
128+
axe._selectCache.push({
129+
selector: selector,
130+
result: result
131+
});
132+
}
117133
return result;
118134
};

test/core/base/audit.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('Audit', function () {
6464
afterEach(function () {
6565
fixture.innerHTML = '';
6666
axe._tree = undefined;
67+
axe._selectCache = undefined;
6768
axe.utils.getFlattenedTree = getFlattenedTree;
6869
});
6970

@@ -479,6 +480,54 @@ describe('Audit', function () {
479480
rules: {}
480481
}, function () {
481482
assert.isTrue(called);
483+
axe.utils.getFlattenedTree = getFlattenedTree;
484+
done();
485+
}, isNotCalled);
486+
});
487+
it('should assign the result of getFlattenedTree to axe._tree', function (done) {
488+
var thing = 'honey badger';
489+
var saved = axe.utils.ruleShouldRun;
490+
axe.utils.ruleShouldRun = function () {
491+
assert.equal(axe._tree, thing);
492+
return false;
493+
};
494+
axe.utils.getFlattenedTree = function () {
495+
return thing;
496+
};
497+
a.run({ include: [document] }, {}, function () {
498+
axe.utils.ruleShouldRun = saved;
499+
done();
500+
}, isNotCalled);
501+
});
502+
it('should clear axe._tree', function (done) {
503+
var thing = 'honey badger';
504+
axe.utils.getFlattenedTree = function () {
505+
return thing;
506+
};
507+
a.run({ include: [document] }, {
508+
rules: {}
509+
}, function () {
510+
assert.isTrue(typeof axe._tree === 'undefined');
511+
axe.utils.getFlattenedTree = getFlattenedTree;
512+
done();
513+
}, isNotCalled);
514+
});
515+
it('should assign an empty array to axe._selectCache', function (done) {
516+
var saved = axe.utils.ruleShouldRun;
517+
axe.utils.ruleShouldRun = function () {
518+
assert.equal(axe._selectCache.length, 0);
519+
return false;
520+
};
521+
a.run({ include: [document] }, {}, function () {
522+
axe.utils.ruleShouldRun = saved;
523+
done();
524+
}, isNotCalled);
525+
});
526+
it('should clear axe._selectCache', function (done) {
527+
a.run({ include: [document] }, {
528+
rules: {}
529+
}, function () {
530+
assert.isTrue(typeof axe._selectCache === 'undefined');
482531
done();
483532
}, isNotCalled);
484533
});

test/core/utils/select.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('axe.utils.select', function () {
1010

1111
afterEach(function () {
1212
fixture.innerHTML = '';
13+
axe._selectCache = undefined;
1314
});
1415

1516

@@ -148,6 +149,17 @@ describe('axe.utils.select', function () {
148149
assert.equal(result.length, 3);
149150

150151
});
152+
it ('should return the cached result if one exists', function () {
153+
fixture.innerHTML = '<div id="zero"><div id="one"><div id="target1" class="bananas"></div></div>' +
154+
'<div id="two"><div id="target2" class="bananas"></div></div></div>';
151155

156+
axe._selectCache = [{
157+
selector: '.bananas',
158+
result: 'fruit bat'
159+
}];
160+
var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('zero'))[0]] });
161+
assert.equal(result, 'fruit bat');
162+
163+
});
152164

153165
});

0 commit comments

Comments
 (0)