Repository navigation
Conversation
Integrate a shared CSS implementation into jsdom, based on https://github.com/cdoublev/css. Replace the separate selector, specificity, and color engines and the existing CSSOM implementation with parsing, matching, cascading, and serialization that share parsed values and participate directly in jsdom's DOM and stylesheet lifecycle. Support custom-property substitution, typed `attr()` and conditional `if()` values, and property registration through `@property` and `CSS.registerProperty()`. Implement cascade layers, nesting, scopes, shadow-tree styling through `:host`, `::slotted()`, and `::part()`, and pseudo-element computed styles. Keep returned computed declarations live across DOM and CSSOM changes. Resolve declaration URLs against their original base URLs, apply display blockification, and handle descriptor-specific grammars and shorthand expansion. Update the HTML and SVG default stylesheets, including namespace restrictions that prevent HTML rules from hiding SVG titles. Reuse candidate rules, selector matches, and individual computed properties within a document's style generation. Preserve layer order and property registrations across DOM mutations while their media conditions still match. Internal single-property reads use the same resolver without creating a public declaration. Match against implementation objects and native tree indexes, and defer CSSOM object creation and declaration parsing until needed. Bound shared syntax caches and keep their contents independent of DOM ownership, preventing unbounded selector-cache growth and retention of the first window used for computed styles. Generate CSS grammars from patched CSSTree data and Webref, and language and directionality tables from maintained registry and Unicode packages. Keep grammar dependencies at build time; runtime parsing and validation use the integrated implementation. Compile initial values and the default stylesheet during preparation to avoid reparsing invariant data at startup. Add regression coverage for cascade precedence, mutation invalidation, substitutions, serialization, and object lifetimes, together with selector and stylesheet scaling benchmarks. Update WPT expectations for the expanded support and link remaining specification and test disagreements to upstream discussions. This remains a non-layout implementation: animation playback, adopted stylesheets, container-query matching, Typed OM, and font loading remain outside its scope. Four-round benchmarks against the parent revision on Node.js 26.8.2 show 13.8× faster `querySelector()` and 4.9× faster `querySelectorAll()` on the valid Sizzle corpus, and 2.7–2.8× faster complex-selector queries. The mutation-and-read benchmark is roughly 5–10× faster than the parent and stays nearly flat as the stylesheet grows from 16 to 1,024 rules; the parent’s cost nearly doubles. The equal mix of DOM creation and styling operations in #3985 is about 2× faster. These gains benefit from repeated syntax and deferred work; fresh declaration assignment and custom-property stylesheet parsing remain about 1.5× and 1.9× slower, respectively. Fixes #1895. Fixes #1928. Fixes #2916. Fixes #2945. Fixes #2999. Fixes #3064. Fixes #3090. Fixes #3366. Fixes #3562. Fixes #3661. Fixes #3785. Fixes #3936. Fixes #3941. Fixes #3993. Fixes #4048. Fixes #4111. Fixes #4132. Fixes #4336. Fixes #4338. Fixes #4378. Fixes #4394. Fixes #4404. Fixes #4409. Closes #4223. Closes #4379. Closes #4405. Closes #4410. Closes #4411. Closes #4412. Closes #4416. Co-authored-by: codex <codex@openai.com>
Reuse immutable declaration parsing contexts for rules with the same grammar, namespace snapshot, and strictness. This avoids allocating a context per rule and repeatedly traversing those contexts when accounting for cached stylesheet syntax. Three-round benchmarks reduce parsing time for 1,024-rule custom-property and ordinary stylesheets by about 6%, and custom-property parsing plus reads by about 8.5%. Co-authored-by: codex <codex@openai.com>
Mark the two editing-host subtests in readwrite-readonly.html as expected failures because document.designMode is not implemented. The previous selector engine honored an expando property set by the test, allowing these cases to pass without implementing the editing state. Keep the remaining 23 subtests checked normally. Co-authored-by: codex <codex@openai.com>
Narrow blanket exclusions to run 68 additional CSS and selector WPT files: 37 pass outright and 31 complete quickly with expected failures. This restores coverage for parsing, serialization, scripted focus, and selector invalidation. Run the full upstream `:has()` complexity test and remove the reduced local duplicate. Replace several unknown timeout reasons with the missing APIs or setup behavior that prevents those tests from running. Co-authored-by: codex <codex@openai.com>
Accept unnamed negated container queries while enforcing reserved container names in shorthands. Normalize recognized style-query declarations and preserve unknown container-query source to match browser serialization, with the unresolved specification requirements documented in the parser. Accept the specified `anchored` container type. Allow `anchor()` and `anchor-size()` in calculations and nested fallbacks while enforcing their host-property restrictions. Keep anchor fallbacks outside the unordered name/side group, reject repeated subgrid auto-fill repetitions, sort and deduplicate computed font variation axes, and serialize string image-set sources as URLs. Remove 14 passing WPT failure expectations and link the remaining test and specification disagreements to upstream PRs and issues. Co-authored-by: codex <codex@openai.com>
Keep main's slow-test skips and retain the passing shadow-root stylesheet test enabled by the CSS implementation. Co-authored-by: codex <codex@openai.com>
Contributor
|
Awesome! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
jsdom's CSS implementation (including selectors) has historically been its weakest link, with new issues opened against it constantly.
Over the years many people have stepped up to help, either directly through jsdom contributions, or through maintaining third-party packages that jsdom came to depend on. These include @dperini's
nwmatcher/nwsapi, @NV'sCSSOMand then @acemir's replacement fork, andcssstylemaintained by first @chad3814, then @jsakas, and finally by the jsdom organization itself. In recent years a lot of work has been done by @asamuzaK, with theirdomSelectorandcssColorpackages, as well as with their many, many contributions directly to jsdom and to cssstyle.However, the mishmash of packages was never a great solution. It balanced the time constraints of the jsdom project (i.e., me) against good architecture: by letting others maintain these parts of jsdom, I could offload some of the maintenance responsibilities to others. This has historically been a reasonable trade for me, but never great for the users of jsdom. With the advent of agentic coding, I started reversing this trend, e.g. moving
cssstyleinto jsdom in 4aaacc6.The most heroic effort to improve jsdom's CSS support came from @cdoublev, who worked for many years to create the
@cdoublev/csspackage by reimplementing ~all of the CSS specifications from scratch. (By hand, as far as I can tell!) In terms of correctness and completeness, the result was much better than the mishmash of historical jsdom code and third-party packages. It also uncovered a ton of mismatches between CSS specifications, tests, and implementations, which was itself a great contribution to the web platform.In #4223, @cdoublev submitted an attempt at integrating his work into jsdom. Unfortunately, the result wasn't something we could accept without significant extra effort. Issues around performance were blocking, and it seemed like the integration with jsdom's internals would take a lot of careful work.
A couple months after that discussion, watching the performance and correctness PRs continue to pour in against jsdom's CSS implementation, I got fed up. Everything needed to be thrown out and replaced, to be at least as correct as
@cdoublev/css. And really, if I were serious about CSS in jsdom, then it all needed to be integrated into jsdom itself. Web browsers aren't split into multiple npm packages, and neither should be jsdom.So I spent 3 banked resets' worth of ChatGPT Astra 6 Ultra tokens, with
@cdoublev/cssas the starting point, with the goal to make it at least as fast as jsdom'smainbranch, but at least as correct as@cdoublev/css. The result is this pull request. It brings in all CSS functionality, including selectors, to the jsdom codebase, fixing tons of correctness bugs and implementing many new features along the way, while being much faster thanmain. Previously this would have been impossible for me to maintain. But I am confident that in the modern age, it will be no problem for me and my agents.(Agents also made it possible for me to meet the standard I requested from @cdoublev in #4223, of producing web platform test PRs or spec clarification issues for every spec/implementation/test mismatch. See below for the full inventory. I'm excited about how this will improve the general CSS spec and test ecosystem for everyone.)
I am trepidatious about the contributor reaction to this, especially from recent active contributors like @asamuzaK and @cdoublev. I want to ensure you realize how valuable your work has been to jsdom. Although jsdom will not be depending on your packages, you've been crucial to getting jsdom to this point, and I would love your help continuing to improve it going forward. We're in a new world, where agentic efforts can allow us to write tons of code that's specification-compliant and fast. For me, this is exciting, and has renewed my interest in working on jsdom. I can understand and accept that you might feel otherwise.
Ultimately, I'm hopeful that we can all appreciate the great step forward that this sort of work is for users of jsdom, and work together on continuing to evolve this fun, quixotic, open-source Node.js reimplementation of a web browser.
Agent-created summary of how this work is based on `@cdoublev/css` and how it departs
The proposed jsdom CSS replacement began with a port of cdoublev/css. Its central design carries through: a grammar-driven parser produces structured values that feed shorthand expansion, serialization, selector matching, and computed styles. The largest changes concern how that engine executes and how its work survives between DOM operations.
A useful starting point is the value representation (cdoublev/css, jsdom). Values carry grammar types alongside their contents; lists carry separators; an explicit sentinel represents omitted components. This lets later stages distinguish values that happen to serialize alike but occupy different grammatical roles. jsdom uses ordinary arrays in place of the original
Listsubclass, but retains this contract. The continuity is especially visible in background expansion (original, jsdom): distributing layers among longhands, supplying omitted components, and taking the color from the final layer. Much of the property-specific knowledge in shorthand expansion, serialization, and parser validation comes directly from this foundation.The grammar parser shows a different kind of continuity. Compare the original transition table with jsdom’s execution loop. The latter replaces table interpretation with explicit control flow while preserving backtracking and the distinction between a candidate match and an accepted parse. That distinction matters when CSS-specific postprocessing rejects something the grammar initially matched. First-token analysis skips impossible alternatives; context-sensitive cases still take the full parsing path. The tokenizer likewise scans through the source string instead of first building a character array. These changes reduce execution and allocation overhead without changing the grammar model.
Object ownership is where the port starts to reshape the surrounding architecture. Upstream constructs a style declaration with each style rule; jsdom creates it when requested. Parsed syntax can be shared through bounded caches while each mutable CSSOM object retains independent state. This also permits deferring declaration validation and value construction. One subtle consequence is that URL context must be captured before those deferred reads: changing a document’s URL must not reinterpret an already-authored declaration when its value is finally inspected.
Computed styles extend that separation across multiple reads. cdoublev/css already resolves individual properties on demand, but its declaration collection rebuilds stylesheet traversal and matching contexts for each property. jsdom gives each element a persistent style state: candidate rules, matching results, and successive stages of property resolution. Reading
colorafterdisplaycan reuse the cascade work without computing every other property. Internal focusability checks use that same resolver to readdisplaywithout constructing a public computed-style object.Layer ordering and property registrations have a longer lifetime than element styles. They survive DOM mutations while the media conditions used to collect them still match; those conditions are rechecked once per style generation. Stylesheet edits, attachment, and removal discard both caches. This avoids walking every stylesheet again just because an element’s class changed. Element invalidation remains document-wide, so rereading unrelated elements still rebuilds their styles.
Selectors benefit from access to the same DOM machinery. Both implementations narrow candidates using IDs, classes, and tags. jsdom’s query planner also considers selective ancestors: for
#panel .item, it can restrict candidate matching to the panel’s subtree when that is cheaper than checking every.item. It works directly with jsdom’s implementation objects, indexes, and ordered collections. That access also matters for correctness. The publicactiveElementgetter retargets focus across shadow boundaries; the matcher needs the actual focused element. Assigned slots, closed shadow roots, and custom-element upgrade state similarly belong to the DOM implementation and are available directly here.The computed-value machinery has also grown beyond the imported implementation. Upstream’s computed-value path leaves arbitrary substitution unfinished. jsdom’s resolver handles substitution, dependency cycles, and registered custom properties within the per-element state. This is a useful place to trace how the inherited typed values acquire element-dependent meaning: parsing
var(--color)is only the beginning of resolving it.Finally, the build takes advantage of inputs jsdom controls. It runs the CSS parser over initial values and the default stylesheet, emitting reusable data without binding it to a particular window. Compare upstream’s configurable environment initialization with jsdom’s default-stylesheet compiler. The data-generation pipeline also combines Webref property metadata with patched CSSTree grammars. Those packages supply build-time data; the runtime parser remains the engine described above. This keeps grammar updates separate from the algorithms needed to implement the features they describe.
Upstream test and specification follow-ups
This work led to 34 WPT pull requests, eleven CSSWG issues, and one Compat issue, plus follow-up comments on existing CSSWG issues. The links include PRs that have already merged.
WPT pull requests
Use noninitial longhands in the
revert-layerfixture (web-platform-tests/wpt#63874)Reset outline color before testing stylesheet values (web-platform-tests/wpt#63875)
Remove optional
positionchecks from pseudo-element variable tests (web-platform-tests/wpt#63878)Expect canonical capitalization in transform functions (web-platform-tests/wpt#63883)
Restrict prefixed generic-family tests to keywords (web-platform-tests/wpt#63884)
Classify negative image resolution as invalid (web-platform-tests/wpt#63885)
Treat dashed page names as valid selectors (web-platform-tests/wpt#63887)
Accept
alpha()without an alpha adjustment (web-platform-tests/wpt#63888) — closed after confirming the existing tests matched a CSSWG resolution; jsdom was corrected instead.Serialize unresolved math roots without an outer
calc()(web-platform-tests/wpt#63889)Expect outline serialization in width/style/color order (web-platform-tests/wpt#63890)
Substitute the initial value of an invalid registered property (web-platform-tests/wpt#63916)
Accept decoded custom-property names in CSSOM setters (web-platform-tests/wpt#63917)
Accept
autowhen parsingcolumn-span(web-platform-tests/wpt#63918)Accept URL sources in
image()parsing tests (web-platform-tests/wpt#63919)Reject two-value caret colors beginning with
auto(web-platform-tests/wpt#63923)Accept single-radius ellipses in
clip-pathparsing (web-platform-tests/wpt#63924)Serialize
max-linesin grammar order (web-platform-tests/wpt#63925)Remove obsolete
invertassumptions from outline fixtures (web-platform-tests/wpt#63926)Serialize
image-resolutionkeywords in grammar order (web-platform-tests/wpt#63928)Serialize resolved
src()controls as URLs (web-platform-tests/wpt#63929)Isolate radial-gradient color-stop parsing (web-platform-tests/wpt#63930)
Preserve authored comments around variable substitution (web-platform-tests/wpt#63931)
Separate element-backed pseudo parsing from matching (web-platform-tests/wpt#63932)
Allow user-action pseudo-classes after pseudo-elements (web-platform-tests/wpt#63933)
Respect legacy
line-clampshorthand representability (web-platform-tests/wpt#63974)Use
attr()fallbacks when the first argument fails to parse (web-platform-tests/wpt#63975)Test Selectors 4 language tag and range requirements (web-platform-tests/wpt#63981)
Distinguish empty custom properties from absent ones (web-platform-tests/wpt#63986)
Include later substitutions in variable cycle detection (web-platform-tests/wpt#63987)
Serialize empty
if()results as a single space (web-platform-tests/wpt#63988)Update registration tests for descriptor defaults (web-platform-tests/wpt#64013)
Collapse equal
font-styledescriptor bounds (web-platform-tests/wpt#64191)Reject extra rules in a
@font-feature-valuesfixture (web-platform-tests/wpt#64192)Allow implementation limits for huge
font-stretchvalues (web-platform-tests/wpt#64193)CSSWG issues
Specify the legacy
zoom: normalalias (w3c/csswg-drafts#14618)Preserve commas and brace blocks in style-query declaration values (w3c/csswg-drafts#14619)
Allow empty
scope()arguments on@import(w3c/csswg-drafts#14620)Reconcile the initial box-shadow color with
box-shadow: none(w3c/csswg-drafts#14621)Define whether computed
path()preserves relative commands (w3c/csswg-drafts#14622)Make polygon rounding radii nonnegative in the grammar (w3c/csswg-drafts#14623)
Clarify single-axis computed
position-areaserialization (w3c/csswg-drafts#14632)Specify retention of default resolutions in
image-set()serialization (w3c/csswg-drafts#14633)Clarify whether raw-string
attr()reads participate in cycle detection (w3c/csswg-drafts#14643)Reconcile feature-value index restrictions with prose and implementations (w3c/csswg-drafts#14657)
Define serialization order for font-feature-value blocks and entries (w3c/csswg-drafts#14658)
Compat issue
currentcolorthrough inheritance for text fill and stroke colors (whatwg/compat#297)Follow-up comments on existing CSSWG issues
if()values (w3c/csswg-drafts#14370)if()style conditions (w3c/csswg-drafts#11143)Full CSS and selector benchmark results, with construction and DOM workloads
Measured on October 25, 2026 against main at
7b741bb7and jsdom v30.1.3 installed from npm, using this PR at00e369a7. This covers all 148 tasks in the 20 suites below: every CSS and selector benchmark, the constructor suite, and DOM construction, HTML parsing, cloning, and focus. The original five workloads from #3985 are included separately. The release comparison includes improvements already present on main; the main comparison measures this PR’s changes.How to read the results
Times are microseconds per complete benchmark operation (1,000 µs = 1 ms). A row that processes many nodes, rules, or queries reports the whole batch. Speedup is baseline time divided by PR time: above 1× is faster; below 1× is slower. These are synthetic API workloads. Small differences should not be treated as significant. There is no combined score across these differently sized workloads.
Method
@asamuzakjp/dom-selector10.0.3 and@asamuzakjp/css-color7.1.3; the release resolved those dependencies to 10.0.5 and 7.1.9.npm run benchmarkrunner was configured for 500 ms of warmup and 1,500 ms of measurement, with at least five measured iterations per task. The cold computed-style, shorthand, and declaration-reading suites instead used 10 warmup iterations and 100 measured iterations, with fixture rebuilding outside the timed region.Comparability notes
N/Ameans the implementation failed the fixture’s correctness preflight. Both baselines accept an invalid selector in the Sizzle corpus, cannot resolve the custom-property substitution fixtures, and lack the combination of cascade layers, registered properties, and live computed styles required by the style-invalidation fixture. These rows have no baseline speedup. The unrelated invalid-selector preflight was isolated from the valid-selector timings.get-computed-styleandget-computed-style-coldsuites only obtain computed-style objects. The property-access, cascade, shorthand, and invalidation suites measure property reads as well.CSS.supports()answers, and the DOM/property values in Performance regression in jsdom@27 #3985 across all three implementations.0pxfor the computed margin/padding defaults where both baselines return0, and uses a shorter computed background shorthand. The release also rearranges declarations in the fresh-assignment fixture. These timings measure the same API workloads, with those output differences.The complex-selector
closest()case had substantial run-to-run variation (v30.1.3: 7.46, 2.58, 2.68 µs; main: 2.65, 2.66, 2.66 µs; PR: 3.29, 1.53, 1.52 µs). Its table entry uses the same median-of-three rule as the other rows; the precise ratio is less reliable than the repeatable query benchmarks.api/constructor
css-supports/supports
dom/cloning
dom/construction
dom/focus
dom/inner-html
selectors/complex-selectors
selectors/mutation-scaling
selectors/sizzle
style/cascade-scaling
style/custom-properties
style/declaration-scaling
style/get-computed-style
style/get-computed-style-cold
style/get-computed-style-property-access
style/get-computed-style-shorthands
style/style-invalidation
style/style-property
style/stylesheet-scaling
style/value-parsing
Original #3985 workloads
Commit message
Integrate a shared CSS implementation into jsdom, based on https://github.com/cdoublev/css. Replace the separate selector, specificity, and color engines and the existing CSSOM implementation with parsing, matching, cascading, and serialization that share parsed values and participate directly in jsdom's DOM and stylesheet lifecycle.
Support custom-property substitution, typed
attr()and conditionalif()values, and property registration through@propertyandCSS.registerProperty(). Implement cascade layers, nesting, scopes, shadow-tree styling through:host,::slotted(), and::part(), and pseudo-element computed styles. Keep returned computed declarations live across DOM and CSSOM changes. Resolve declaration URLs against their original base URLs, apply display blockification, and handle descriptor-specific grammars and shorthand expansion. Update the HTML and SVG default stylesheets, including namespace restrictions that prevent HTML rules from hiding SVG titles.Reuse candidate rules, selector matches, and individual computed properties within a document's style generation. Preserve layer order and property registrations across DOM mutations while their media conditions still match. Internal single-property reads use the same resolver without creating a public declaration. Match against implementation objects and native tree indexes, and defer CSSOM object creation and declaration parsing until needed. Bound shared syntax caches and keep their contents independent of DOM ownership, preventing unbounded selector-cache growth and retention of the first window used for computed styles.
Generate CSS grammars from patched CSSTree data and Webref, and language and directionality tables from maintained registry and Unicode packages. Keep grammar dependencies at build time; runtime parsing and validation use the integrated implementation. Compile initial values and the default stylesheet during preparation to avoid reparsing invariant data at startup.
Add regression coverage for cascade precedence, mutation invalidation, substitutions, serialization, and object lifetimes, together with selector and stylesheet scaling benchmarks. Update WPT expectations for the expanded support and link remaining specification and test disagreements to upstream discussions. This remains a non-layout implementation: animation playback, adopted stylesheets, container-query matching, Typed OM, and font loading remain outside its scope.
Four-round benchmarks against the parent revision on Node.js 26.8.2 show 13.8× faster
querySelector()and 4.9× fasterquerySelectorAll()on the valid Sizzle corpus, and 2.7–2.8× faster complex-selector queries. The mutation-and-read benchmark is roughly 5–10× faster than the parent and stays nearly flat as the stylesheet grows from 16 to 1,024 rules; the parent’s cost nearly doubles. The equal mix of DOM creation and styling operations in #3985 is about 2× faster. These gains benefit from repeated syntax and deferred work; fresh declaration assignment and custom-property stylesheet parsing remain about 1.5× and 1.9× slower, respectively.Fixes #1895. Fixes #1928. Fixes #2916. Fixes #2945. Fixes #2999. Fixes #3064. Fixes #3090. Fixes #3366. Fixes #3562. Fixes #3661. Fixes #3785. Fixes #3936. Fixes #3941. Fixes #3993. Fixes #4048. Fixes #4111. Fixes #4132. Fixes #4336. Fixes #4338. Fixes #4378. Fixes #4394. Fixes #4404. Fixes #4409.
Closes #4223. Closes #4379. Closes #4405. Closes #4410. Closes #4411. Closes #4412. Closes #4416.