Skip to content

Selector cache hit rate falls from 100% to 0% at CACHE_LIMIT + 1 under jsdom's stylesheet scan (2.2.25+); #185/#186 are not in master #242

Open
DataBiosphere/findable-ui
#1169

Description

@davidwilder

Since 2.2.25 the compiled-selector caches are a strict LRU bounded at CACHE_LIMIT = 1000 (2.2.28 src/nwsapi.js:236, eviction at :290). jsdom's getComputedStyle matches every stylesheet rule against each element in sheet order, which is a loop. On a loop of N > limit selectors, LRU always evicts the one the scan needs next, so the hit rate drops from 100% to 0% at N = limit + 1 and every match recompiles. A page carrying antd's CSS-in-JS has about 2,300 rules. One of our jsdom test files went from 3.7 s on 2.2.22 to a 37 s timeout on 2.2.28, with about 1.05M recompiles. With the limit raised to 1e6 it runs in 6.8 s.

Reproducer (npm i jsdom nwsapi@2.2.28):

const { JSDOM } = require('jsdom'), nwsapi = require('nwsapi');
for (const N of [1000, 1001]) {
  const { window } = new JSDOM('<p class="c0">'), el = window.document.querySelector('p');
  const nw = nwsapi({ document: window.document, DOMException: window.DOMException });
  const sels = Array.from({ length: N }, (_, i) => `.c${i}`);
  sels.forEach(s => nw.match(s, el)); // first pass compiles each selector once
  let miss = 0;
  for (let pass = 0; pass < 10; pass++) for (const s of sels) { miss += !nw.matchResolvers.has(s); nw.match(s, el); }
  console.log(`${N} selectors: ${miss} cache misses in ${N * 10} matches`);
}

Output: 1000 selectors: 0 cache misses in 10000 matches, then 1001 selectors: 10010 cache misses in 10010 matches.

#185 (two-generation eviction) and #186 (limit 4096) were merged on 2026-09-07, but neither merge commit is in current master or in 2.2.28. Master (8b622b5) also adds CACHE_BYTES = 2 MB, which evicts from the same head. Even with the entry limit at 100,000, selectors shaped like :where(.css-x).ant-btn-N:not(:disabled):not(.ant-btn-disabled):hover miss on every match from N = 1,254.

Could the next 2.2.x (a) restore bounds that cover a real stylesheet, or let the host set them (jsdom already calls configure(), which accepts only booleans today), and (b) evict so that a scan larger than the cache keeps a resident subset hitting instead of none?

Activity

  1. dperini commented on Oct 8, 2026

    @dperini
    Owner

    @davidwilder
    thank you for the review of this functionality (now forgotten/missed).

    The only reason that pushed us using a different approach for the size of the cache instead of mixing it in a series of flags to enable/disable features. A number representing the configurable size of the cached resolvers was a reasoned pick that I would hope to still yield what were the original expectations from the configure method initialization.

    Haven't implemented it completely yet but the idea was to make the last arguments of configure would represent a Number().

    I am open to alternative suggestions to get the same functionality (as in configure) and covering the obvious needs depicted above.

  2. dperini commented on Oct 10, 2026

    @dperini
    Owner

    @davidwilder
    can you please check if the changes in #243 satisfy the expressed needs.

    Thanks @jdalton for the quick fix.

  3. jdalton commented on Oct 11, 2026

    @jdalton
    Collaborator

    V3 retains useful compiled plans during oversized scans and supports configurable cache budgets. See Selector caching.

    #246 brings the admission policy to V2, following the configurable limits in #243.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions