Skip to content

Answer a constant nth-child index without building the sibling list - #184

Open
jdalton wants to merge 1 commit into
dperini:masterfrom
jdalton:perf/nth-constant
Open

Answer a constant nth-child index without building the sibling list#184
jdalton wants to merge 1 commit into
dperini:masterfrom
jdalton:perf/nth-constant

Conversation

@jdalton

@jdalton jdalton commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

:nth-child(3) builds the whole sibling list of the parent to compare an index against a constant. Counting back at most b siblings and stopping once the index is exceeded is two and a half times faster, and leaves the an+b and of-type forms on the cached list where that trade still pays.

Detail, and how it was checked

:nth-child(3) compiles to n=s.nthElement(e,false) followed by n==3, and nthElement numbers an element by building the sibling list of its parent. That is the right trade for an an+b form, which has to know where the element sits, and pure overhead for a constant index, which only has to know whether three steps back runs out of siblings. The generated code now counts siblings and stops as soon as the index is exceeded, so it walks at most b of them and allocates nothing.

selector before after
div:nth-child(3) 115.99 us 46.54 us 2.49x
div:nth-last-child(3) 115.18 us 46.08 us 2.50x
div:nth-child(7) 115.47 us 81.02 us 1.43x
li:nth-child(2) 253.96 us 197.74 us 1.28x

Only the -child forms. Of-type has to compare the name of every sibling it steps over, and reading localName through the host on each one costs more than the list it avoids — measured 2.0x and 2.6x slower than the cached list for :nth-of-type(3) and :nth-last-of-type(3) — so those keep it. The an+b forms are untouched: :nth-child(2n) and :nth-child(n+3) still need the index. Results agree with the native engine on every form tested.

Extracted from #167 as a standalone change: one file, applies to master on its own, and checked against the benchmark fixture to confirm the results are identical and nothing else moves.

References: the spec, the browser source, and what each part was reasoned from

This patch applies to master on its own. The sixteen in this series were checked by cherry-picking them onto master one after another, in this order and in reverse, and all sixteen land without a conflict.

':nth-child(3)' compiles to n=s.nthElement(e,false) followed by n==3, and nthElement numbers an element by building the sibling list of its parent. That is the right trade for an an+b form, which has to know where the element sits, and pure overhead for a constant index, which only has to know whether three steps back runs out of siblings. The generated code now counts siblings and stops as soon as the index is exceeded, so it walks at most b of them and allocates nothing.
 div:nth-child(3) 115.99us -> 46.54us 2.49x div:nth-last-child(3) 115.18us -> 46.08us 2.50x div:nth-child(7) 115.47us -> 81.02us 1.43x li:nth-child(2) 253.96us -> 197.74us 1.28x
 Only the -child forms. Of-type has to compare the name of every sibling it steps over, and reading localName through the host on each one costs more than the list it avoids — measured 2.0x and 2.6x slower than the cached list for ':nth-of-type(3)' and ':nth-last-of-type(3)' — so those keep it. The an+b forms are untouched: ':nth-child(2n)' and ':nth-child(n+3)' still need the index. Results agree with the native engine on every form tested.

References:

- Spec: https://drafts.csswg.org/selectors-4/#nth-child-pseudo — the An+B forms, of which a constant index is one
- Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/css/selector_checker.cc#L2443 — ':nth-child' goes through a cache of sibling indexes
- Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/dom/nth_index_cache.h — that cache, which is the same trade this patch avoids for a constant
- MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant