Skip to content

Commit 8137a0d

Browse files
committed
perf: cache getSelectorsList results
1 parent 95dd1e7 commit 8137a0d

1 file changed

Lines changed: 54 additions & 6 deletions

File tree

src/utilities-selectors.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ import { isElement } from "./utilities-iselement.js";
3535
import { getPowerSet, powerSetGenerator } from "./utilities-powerset.js";
3636
import { cartesianProductGenerator } from "./utilities-cartesian.js";
3737

38-
export const ESCAPED_COLON: string = ":".charCodeAt(0).toString(16).toUpperCase();
38+
export const ESCAPED_COLON: string = ":"
39+
.charCodeAt(0)
40+
.toString(16)
41+
.toUpperCase();
3942

4043
// Square brackets need to be escaped, but eslint has a problem with that.
4144
/* eslint-disable-next-line no-useless-escape */
42-
export const SPECIAL_CHARACTERS_RE: RegExp = /[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;
45+
export const SPECIAL_CHARACTERS_RE: RegExp =
46+
/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;
4347

4448
/**
4549
* Escapes special characters used by CSS selector items.
@@ -87,7 +91,10 @@ export const SELECTOR_TYPE_GETTERS: Record<
8791

8892
export const ELEMENT_SELECTOR_TYPE_GETTERS: Record<
8993
CssSelectorType,
90-
(element: Element, options?: CssSelectorGeneratorOptions) => CssSelectorGenerated[]
94+
(
95+
element: Element,
96+
options?: CssSelectorGeneratorOptions,
97+
) => CssSelectorGenerated[]
9198
> = {
9299
tag: getElementTagSelectors,
93100
id: getElementIdSelectors,
@@ -151,15 +158,43 @@ export function orderSelectors(
151158
});
152159
}
153160

161+
export type SelectorsListCache = (
162+
elements: Element[],
163+
options: CssSelectorGeneratorOptions,
164+
) => CssSelectorData;
165+
166+
/**
167+
* Creates a cache for `getSelectorsList` results, scoped to a single selector-generation run.
168+
*/
169+
export function createSelectorsListCache(): SelectorsListCache {
170+
const cache = new Map<Element, CssSelectorData>();
171+
let multiElementResult: CssSelectorData | undefined;
172+
173+
return (elements, options) => {
174+
if (elements.length !== 1) {
175+
// multi-element needle is always the same array reference within one generator run
176+
return (multiElementResult ??= getSelectorsList(elements, options));
177+
}
178+
const [element] = elements;
179+
let result = cache.get(element);
180+
if (!result) {
181+
result = getSelectorsList(elements, options);
182+
cache.set(element, result);
183+
}
184+
return result;
185+
};
186+
}
187+
154188
/**
155189
* Yields list of unique selectors applicable to given element.
156190
*/
157191
export function* allSelectorsGenerator(
158192
elements: Element[],
159193
options: CssSelectorGeneratorOptions,
194+
selectorsListCache: SelectorsListCache = createSelectorsListCache(),
160195
): IterableIterator<CssSelector> {
161196
const yieldedSelectors = new Set<string>();
162-
const selectors_list = getSelectorsList(elements, options);
197+
const selectors_list = selectorsListCache(elements, options);
163198
for (const selector of selectorTypeCombinationsGenerator(
164199
selectors_list,
165200
options,
@@ -185,7 +220,11 @@ export function getSelectorsList(
185220
const matchWhitelist = createPatternMatcher(whitelist);
186221

187222
const reducer = (data: CssSelectorData, selector_type: CssSelectorType) => {
188-
const selectors_by_type = getSelectorsByType(elements, selector_type, options);
223+
const selectors_by_type = getSelectorsByType(
224+
elements,
225+
selector_type,
226+
options,
227+
);
189228
const filtered_selectors = filterSelectors(
190229
selectors_by_type,
191230
matchBlacklist,
@@ -358,8 +397,13 @@ export function* selectorWithinRootGenerator(
358397
root: ParentNode,
359398
rootSelector: CssSelector | undefined = "",
360399
options: CssSelectorGeneratorOptions,
400+
selectorsListCache: SelectorsListCache = createSelectorsListCache(),
361401
): IterableIterator<CssSelector, undefined> {
362-
const elementSelectorsIterator = allSelectorsGenerator(elements, options);
402+
const elementSelectorsIterator = allSelectorsGenerator(
403+
elements,
404+
options,
405+
selectorsListCache,
406+
);
363407
for (const candidateSelector of candidatesGenerator(
364408
elementSelectorsIterator,
365409
rootSelector,
@@ -379,6 +423,7 @@ export function* closestIdentifiableParentGenerator(
379423
root: ParentNode,
380424
rootSelector: CssSelector | undefined = "",
381425
options: CssSelectorGeneratorOptions,
426+
selectorsListCache: SelectorsListCache = createSelectorsListCache(),
382427
): IterableIterator<IdentifiableParent> {
383428
if (elements.length === 0) {
384429
return null;
@@ -395,6 +440,7 @@ export function* closestIdentifiableParentGenerator(
395440
root,
396441
rootSelector,
397442
options,
443+
selectorsListCache,
398444
)) {
399445
yield {
400446
foundElements: currentElements,
@@ -423,6 +469,7 @@ export function* selectorGenerator({
423469
let currentRoot = root;
424470
let partialSelector = rootSelector;
425471
let shouldContinue = true;
472+
const selectorsListCache = createSelectorsListCache();
426473

427474
while (shouldContinue) {
428475
let foundAny = false;
@@ -432,6 +479,7 @@ export function* selectorGenerator({
432479
currentRoot,
433480
partialSelector,
434481
options,
482+
selectorsListCache,
435483
)) {
436484
const { foundElements, selector } = item;
437485
foundAny = true;

0 commit comments

Comments
 (0)