Skip to content

Commit d9ad334

Browse files
TomPridhameps1lon
andauthored
feat: store getComputedStyle result to avoid redoing that work (#1048)
Co-authored-by: eps1lon <[email protected]>
1 parent 8ba404d commit d9ad334

File tree

4 files changed

+75
-7
lines changed

4 files changed

+75
-7
lines changed

.changeset/spotty-chicken-add.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"dom-accessibility-api": minor
3+
---
4+
5+
Cache `window.getComputedStyle` results
6+
7+
Should improve performance in environments that don't cache these results natively e.g. JSDOM.
8+
This increases memory usage.
9+
If this results in adverse effects (e.g. resource constrained browser environments), please file an issue.

sources/__tests__/accessible-name.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,15 @@ describe("options.getComputedStyle", () => {
591591
expect(name).toEqual("foo test foo");
592592
expect(window.getComputedStyle).not.toHaveBeenCalled();
593593
});
594+
it("is not called more than once per element", () => {
595+
const container = renderIntoDocument(
596+
"<button><span><span>nested</span>button</span></button>",
597+
);
598+
599+
computeAccessibleName(container.querySelector("button"));
600+
// once for the button, once for each span
601+
expect(window.getComputedStyle).toHaveBeenCalledTimes(3);
602+
});
594603
});
595604

596605
describe("options.computedStyleSupportsPseudoElements", () => {

sources/accessible-name-and-description.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type FlatString = string & {
3030
__flat: true;
3131
};
32+
type GetComputedStyle = typeof window.getComputedStyle;
3233

3334
/**
3435
* interface for an options-bag where `window.getComputedStyle` can be mocked
@@ -43,7 +44,7 @@ export interface ComputeTextAlternativeOptions {
4344
/**
4445
* mock window.getComputedStyle. Needs `content`, `display` and `visibility`
4546
*/
46-
getComputedStyle?: typeof window.getComputedStyle;
47+
getComputedStyle?: GetComputedStyle;
4748
/**
4849
* Set to `true` if you want to include hidden elements in the accessible name and description computation.
4950
* Skips 2A in https://w3c.github.io/accname/#computation-steps.
@@ -69,7 +70,7 @@ function asFlatString(s: string): FlatString {
6970
*/
7071
function isHidden(
7172
node: Node,
72-
getComputedStyleImplementation: typeof window.getComputedStyle,
73+
getComputedStyleImplementation: GetComputedStyle,
7374
): node is Element {
7475
if (!isElement(node)) {
7576
return false;
@@ -338,6 +339,10 @@ export function computeTextAlternative(
338339
options: ComputeTextAlternativeOptions = {},
339340
): string {
340341
const consultedNodes = new SetLike<Node>();
342+
const computedStyles =
343+
typeof Map === "undefined"
344+
? undefined
345+
: new Map<Element, CSSStyleDeclaration>();
341346

342347
const window = safeWindow(root);
343348
const {
@@ -348,9 +353,36 @@ export function computeTextAlternative(
348353
// window.getComputedStyle(elementFromAnotherWindow) or if I don't bind it
349354
// the type declarations don't require a `this`
350355
// eslint-disable-next-line no-restricted-properties
351-
getComputedStyle = window.getComputedStyle.bind(window),
356+
getComputedStyle: uncachedGetComputedStyle = window.getComputedStyle.bind(
357+
window,
358+
),
352359
hidden = false,
353360
} = options;
361+
const getComputedStyle: GetComputedStyle = (
362+
el,
363+
pseudoElement,
364+
): CSSStyleDeclaration => {
365+
// We don't cache the pseudoElement styles and calls with psuedo elements
366+
// should use the uncached version instead
367+
if (pseudoElement !== undefined) {
368+
throw new Error(
369+
"use uncachedGetComputedStyle directly for pseudo elements",
370+
);
371+
}
372+
// If Map is not available, it is probably faster to just use the uncached
373+
// version since a polyfill lookup would be O(n) instead of O(1) and
374+
// the getComputedStyle function in those environments(e.g. IE11) is fast
375+
if (computedStyles === undefined) {
376+
return uncachedGetComputedStyle(el);
377+
}
378+
const cachedStyles = computedStyles.get(el);
379+
if (cachedStyles) {
380+
return cachedStyles;
381+
}
382+
const style = uncachedGetComputedStyle(el, pseudoElement);
383+
computedStyles.set(el, style);
384+
return style;
385+
};
354386

355387
// 2F.i
356388
function computeMiscTextAlternative(
@@ -359,7 +391,7 @@ export function computeTextAlternative(
359391
): string {
360392
let accumulatedText = "";
361393
if (isElement(node) && computedStyleSupportsPseudoElements) {
362-
const pseudoBefore = getComputedStyle(node, "::before");
394+
const pseudoBefore = uncachedGetComputedStyle(node, "::before");
363395
const beforeContent = getTextualContent(pseudoBefore);
364396
accumulatedText = `${beforeContent} ${accumulatedText}`;
365397
}
@@ -384,9 +416,8 @@ export function computeTextAlternative(
384416
// trailing separator for wpt tests
385417
accumulatedText += `${separator}${result}${separator}`;
386418
});
387-
388419
if (isElement(node) && computedStyleSupportsPseudoElements) {
389-
const pseudoAfter = getComputedStyle(node, "::after");
420+
const pseudoAfter = uncachedGetComputedStyle(node, "::after");
390421
const afterContent = getTextualContent(pseudoAfter);
391422
accumulatedText = `${accumulatedText} ${afterContent}`;
392423
}
@@ -564,7 +595,6 @@ export function computeTextAlternative(
564595
if (consultedNodes.has(current)) {
565596
return "";
566597
}
567-
568598
// 2A
569599
if (
570600
!hidden &&

sources/polyfills/Map.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
declare global {
2+
class Map<K, V> {
3+
// es2015.collection.d.ts
4+
clear(): void;
5+
delete(key: K): boolean;
6+
forEach(
7+
callbackfn: (value: V, key: K, map: Map<K, V>) => void,
8+
thisArg?: unknown,
9+
): void;
10+
get(key: K): V | undefined;
11+
has(key: K): boolean;
12+
set(key: K, value: V): this;
13+
readonly size: number;
14+
}
15+
}
16+
17+
// we need to export something here to make this file a module, but don't want to
18+
// actually include a polyfill because it's potentially significantly slower than
19+
// the native implementation
20+
export {};

0 commit comments

Comments
 (0)