diff --git a/cjs/shared/matches.js b/cjs/shared/matches.js index e4deed74..86785ccd 100644 --- a/cjs/shared/matches.js +++ b/cjs/shared/matches.js @@ -108,14 +108,29 @@ const prepareMatch = (element, selectors) => CSSselect.compile( ); exports.prepareMatch = prepareMatch; -const matches = (element, selectors) => CSSselect.is( - element, - selectors, - { - strict: true, - context: selectors.includes(':scope') ? element : void 0, - xmlMode: !ignoreCase(element), - adapter +// matches() is called once per element and selector pair, so the selector +// is compiled once and kept, instead of parsed again on every call. +// :scope depends on the element, so those are compiled each time. +const compiled = new Map; +const MAX_COMPILED = 1024; + +/** @returns {boolean} */ +const matches = (element, selectors) => { + const xmlMode = !ignoreCase(element); + if (selectors.includes(':scope')) + return CSSselect.is( + element, + selectors, + {strict: true, context: element, xmlMode, adapter} + ); + const key = (xmlMode ? 'x' : 'h') + selectors; + let test = compiled.get(key); + if (!test) { + if (compiled.size === MAX_COMPILED) + compiled.delete(compiled.keys().next().value); + test = CSSselect.compile(selectors, {strict: true, xmlMode, adapter}); + compiled.set(key, test); } -); + return test(element); +}; exports.matches = matches; diff --git a/esm/shared/matches.js b/esm/shared/matches.js index dbc3339a..3012e470 100644 --- a/esm/shared/matches.js +++ b/esm/shared/matches.js @@ -106,13 +106,28 @@ export const prepareMatch = (element, selectors) => CSSselect.compile( } ); -export const matches = (element, selectors) => CSSselect.is( - element, - selectors, - { - strict: true, - context: selectors.includes(':scope') ? element : void 0, - xmlMode: !ignoreCase(element), - adapter +// matches() is called once per element and selector pair, so the selector +// is compiled once and kept, instead of parsed again on every call. +// :scope depends on the element, so those are compiled each time. +const compiled = new Map; +const MAX_COMPILED = 1024; + +/** @returns {boolean} */ +export const matches = (element, selectors) => { + const xmlMode = !ignoreCase(element); + if (selectors.includes(':scope')) + return CSSselect.is( + element, + selectors, + {strict: true, context: element, xmlMode, adapter} + ); + const key = (xmlMode ? 'x' : 'h') + selectors; + let test = compiled.get(key); + if (!test) { + if (compiled.size === MAX_COMPILED) + compiled.delete(compiled.keys().next().value); + test = CSSselect.compile(selectors, {strict: true, xmlMode, adapter}); + compiled.set(key, test); } -); + return test(element); +}; diff --git a/test/html/document.js b/test/html/document.js index b5e15062..eedd4f17 100644 --- a/test/html/document.js +++ b/test/html/document.js @@ -92,6 +92,18 @@ emptyClass.getElementById('b').className = 'x'; emptyClass.getElementById('b').classList.remove('x'); assert(emptyClass.querySelectorAll('[class=""]').map(({id}) => id).join(','), 'b,c', '[class=""] follows class mutations'); +const {document: compiled} = parseHTML('