Skip to content

Read the class and the id as properties, and compare the id - #194

Open
jdalton wants to merge 1 commit into
dperini:masterfrom
jdalton:perf/property-reads
Open

Read the class and the id as properties, and compare the id#194
jdalton wants to merge 1 commit into
dperini:masterfrom
jdalton:perf/property-reads

Conversation

@jdalton

@jdalton jdalton commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two of the tests a compiled resolver runs per candidate ask the host for an attribute whose value is already reflected as a property, and one of them matches a pattern where the selector means an exact comparison.

What the two tests do today, what they measure, and where nothing changes

A class test calls getAttribute('class'), which is a call through the host where Element.className is a property read. An id test compiles to a regular expression over getAttribute('id'), where the selector is asking whether the id equals one string.

Interleaved against master in one process, on the 6344-element fixture in test/speed/example/selectors.html:

selector master patched
div.example > p 0.1006 ms 0.0804 ms 1.25x
#title p 0.3118 ms 0.2411 ms 1.29x
.example a 2.3874 ms 2.1421 ms 1.11x
.example 0.0075 ms 0.0075 ms unchanged
div.example 0.0092 ms 0.0091 ms unchanged

The last two rows are the point of the table as much as the first three. collect() fetches candidates by the last simple token of the selector, so for .example the class is what the fetch asked the host for and no resolver tests it afterwards. The gain only appears where a class or an id is tested per candidate, which is when it is not the token the fetch used.

The per-read costs behind that, one read per element over the same document: e.className 0.477 ms against getAttribute('class') 0.851 ms, and comparing e.id 0.383 ms against a pattern over getAttribute('id') 0.717 ms.

The SVG reflection, which is why the class read is not a one-liner

Element.className is a string, except on an SVG element, where SVG 1.1 defined SVGElement.className as an SVGAnimatedString and SVG 2 deprecated it without removing it. Chrome, Safari, Firefox and jsdom all still expose it.

Reading it without checking the type matches the class against [object SVGAnimatedString] and quietly finds nothing, so classOf() checks the type, reads baseVal when it is there, and asks for the attribute otherwise. Eleven selectors covering classes, escaped ids, SVG classes on <svg> and <rect>, and a negation answer the same as the reference engine before and after this patch.

How this sits with the other patches in the series

It applies to master on its own. The seventeen patches extracted from #167 were checked by cherry-picking them onto master one after another, in PR order and in reverse, and all seventeen land without a conflict — the helper here sits next to byClass(), which no other patch anchors on, and its export goes to a spaced-out key in the snapshot list.

Two of the tests the resolvers run per candidate ask the host for an attribute where the same value is reflected as a property, and one of them matches a pattern where the selector means an exact comparison.
 A class test calls getAttribute('class'). The class attribute is reflected as Element.className, and reading a property is cheaper than calling through the host: 0.477ms against 0.851ms over 6344 elements in jsdom. The reflection is a string on an HTML element and an SVGAnimatedString on an SVG one, which SVG 1.1 defined and SVG 2 deprecated without removing, so classOf() checks the type and asks for the attribute when it is not a string. Reading it without that check matches the class against '[object SVGAnimatedString]' and quietly finds nothing.
 An id test compiles to a regular expression over getAttribute('id'), where the selector asks whether the id equals one string. Comparing e.id measures 0.383ms against 0.717ms on the same document. The escapes have to survive the change, since a comparison holds a string where the pattern held a pattern, so the value goes through escapeIdentifier the way an attribute value already does.
 Browsers do both: Blink matches a class against a parsed token list and compares an id for equality rather than matching it.

References:

- Spec: https://dom.spec.whatwg.org/#dom-element-classname — className reflects the class attribute
- Spec: https://dom.spec.whatwg.org/#dom-element-id — and id reflects the id attribute
- Spec: https://svgwg.org/svg2-draft/types.html#__svg__SVGElement__className — the SVG reflection that is not a string, deprecated but still shipping
- Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/css/selector_checker.cc#L1532 — a class is matched against a parsed token list, not a string scan
- Chromium: https://github.com/chromium/chromium/blob/155.0.8041.1/third_party/blink/renderer/core/css/selector_checker.cc#L1536 — an id is compared for equality
- MDN: https://developer.mozilla.org/en-US/docs/Web/API/Element/className
@jdalton
jdalton force-pushed the perf/property-reads branch from a8537b4 to 04dd913 Compare September 5, 2026 02:47
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