Skip to content

Commit f5d9c60

Browse files
Juliette Pretotjul-sh
Juliette Pretot
authored andcommitted
use a queue of pending audits to run on idle
1 parent aceac5b commit f5d9c60

5 files changed

+76
-58
lines changed

AuditQueue.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { rIC as requestIdleCallback } from 'idlize/idle-callback-polyfills.mjs'
2+
3+
export default class AuditQueue {
4+
constructor() {
5+
this._pendingAudits = []
6+
this._isRunning = false
7+
8+
this.run = this.run.bind(this)
9+
this._scheduleAudits = this._scheduleAudits.bind(this)
10+
}
11+
run(getAuditResult) {
12+
// Returns a promise that resolves to the result of the auditCallback.
13+
return new Promise((resolve, reject) => {
14+
const runAudit = async () => {
15+
try {
16+
const result = await getAuditResult()
17+
resolve(await result)
18+
} catch (error) {
19+
reject(error)
20+
}
21+
}
22+
23+
this._pendingAudits.push(runAudit)
24+
if (!this._isRunning) this._scheduleAudits()
25+
})
26+
}
27+
_scheduleAudits() {
28+
this._isRunning = true
29+
requestIdleCallback(async IdleDeadline => {
30+
// Run pending audits as long as they exist & we have time.
31+
while (
32+
this._pendingAudits.length > 0 &&
33+
IdleDeadline.timeRemaining() > 0
34+
) {
35+
// Only run one audit at a time, as axe-core does not allow for
36+
// concurrent runs.
37+
// Ref: https://github.com/dequelabs/axe-core/issues/1041
38+
const runAudit = this._pendingAudits[0]
39+
await runAudit()
40+
41+
// Once an audit has run, remove it from the queue.
42+
this._pendingAudits.shift()
43+
}
44+
45+
if (this._pendingAudits.length > 0) {
46+
// If pending audits remain, schedule them for the next idle phase.
47+
this._scheduleAudits()
48+
} else {
49+
// The queue is empty, we're no longer running
50+
this._isRunning = false
51+
}
52+
})
53+
}
54+
}

AxeObserver.js

+13-33
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
import axeCore from 'axe-core'
2-
import PQueue from 'p-queue'
3-
4-
// If requestIdleCallback is not supported, we fallback to setTimeout
5-
// Ref: https://developers.google.com/web/updates/2015/08/using-requestidlecallback
6-
const requestIdleCallback =
7-
window.requestIdleCallback ||
8-
function(callback) {
9-
setTimeout(callback, 1)
10-
}
11-
12-
// axe-core cannot be run concurrently. There are no plans to implement this
13-
// functionality or a queue. Hence we use PQueue to queue calls ourselves.
14-
// Ref: https://github.com/storybookjs/storybook/pull/4086
15-
const axeQueue = new PQueue({ concurrency: 1 })
2+
import AuditQueue from './AuditQueue.js'
163

174
// The AxeObserver class takes a violationsCallback, which is invoked with an
185
// array of observed violations.
@@ -45,15 +32,18 @@ export default class AxeObserver {
4532

4633
this.observe = this.observe.bind(this)
4734
this.disconnect = this.disconnect.bind(this)
48-
this._scheduleAudit = this._scheduleAudit.bind(this)
35+
this._auditNode = this._auditNode.bind(this)
4936

5037
this._alreadyReportedIncidents = new Set()
5138
this._mutationObserver = new window.MutationObserver(mutationRecords => {
5239
mutationRecords.forEach(mutationRecord => {
53-
this._scheduleAudit(mutationRecord.target)
40+
this._auditNode(mutationRecord.target)
5441
})
5542
})
5643

44+
// AuditQueue sequentially runs audits when the browser is idle.
45+
this._auditQueue = new AuditQueue()
46+
5747
// Allow for registering plugins etc
5848
if (typeof axeInstanceCallback === 'function') {
5949
axeCoreInstanceCallback(axeCore)
@@ -73,27 +63,17 @@ export default class AxeObserver {
7363
})
7464

7565
// run initial audit on the whole targetNode
76-
this._scheduleAudit(targetNode)
66+
this._auditNode(targetNode)
7767
}
7868
disconnect() {
7969
this.mutationObserver.disconnect()
8070
}
81-
async _scheduleAudit(node) {
82-
const response = await axeQueue.add(
83-
() =>
84-
new Promise(resolve => {
85-
requestIdleCallback(
86-
() => {
87-
// Since audits are scheduled asynchronously, it can happen that
88-
// the node is no longer connected. We cannot analyze it then.
89-
node.isConnected ? axeCore.run(node).then(resolve) : resolve(null)
90-
},
91-
// Time after which an audit will be performed, even if it may
92-
// negatively affect performance.
93-
{ timeout: 1000 }
94-
)
95-
})
96-
)
71+
async _auditNode(node) {
72+
const response = await this._auditQueue.run(async () => {
73+
// Since audits are scheduled asynchronously, it can happen that
74+
// the node is no longer connected. We cannot analyze it then.
75+
return node.isConnected ? axeCore.run(node) : null
76+
})
9777

9878
if (!response) return
9979

README.MD

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The `AxeObserver` constructor takes two parameters:
4646
- `axeCoreConfiguration` (optional). A configuration object for axe-core. Read about the object here in the [axe-core docs](https://github.com/dequelabs/axe-core/blob/master/doc/API.md#api-name-axeconfigure) and see the `agnostic-axe` source code for the default options used.
4747
- `axeCoreInstanceCallback` (optional). A function that is invoked with the instance of [axe-core](https://github.com/dequelabs/axe-core) used by module. It can be used for complex interactions with the [axe-core API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md) such as registering plugins.
4848

49-
The `AxeObserver.observe` method takes two parameters:
49+
The `AxeObserver.observe` method takes one parameter:
5050

5151
- `targetNode` (required). The node that should be observed & analyzed.
5252

package-lock.json

+7-23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@
4343
},
4444
"dependencies": {
4545
"axe-core": "^3.4.0",
46-
"p-queue": "^6.2.1"
46+
"idlize": "^0.1.1"
4747
}
4848
}

0 commit comments

Comments
 (0)