LibWeb: Move the style system's inner loops into Rust - #10787
Merged
Conversation
The style port needs to measure which C++/Rust seams dominate before removing them. Add a per-operation counter table to the CSS Rust crate, bumped at every entry point into the style core and at every callback the core makes back into C++: the longhand driver stages, cascade origin stages, declaration application, shorthand expansion, selector DOM callbacks, calc operations, style value shell retain/release, and style group clone/free. The counters are always-on relaxed atomics; one increment per crossing is negligible next to the crossing itself. The counters are exposed to tests as internals.styleFfiCounters() and internals.resetStyleFfiCounters(), and a new test covers that surface. Baseline measurements on deterministic workloads show the per-longhand computation loop dominating every other boundary by two orders of magnitude: roughly 1330 crossings per recomputed element, of which 333 are compute_and_store callbacks, ~330 are initial and inherited value fetches, and ~980 are per-longhand style value queries made from C++.
Deterministic pages measuring the style FFI boundary counters over representative DOM shapes: a small document, a stylesheet-heavy document, an inheritance-heavy chain, custom properties, shadow DOM with slots and parts, animations and transitions, and a targeted single-element restyle. collect.py copies them into the LibWeb test tree, runs them through test-web, prints each workload's counters, and cleans up again. The output is a measurement of the current boundary, not a regression test, so the pages stay out of the committed suite.
The longhand computation loop calls back into C++ once per property that falls back to its initial value, only to have C++ look the value up in a process-wide table. Pin every longhand's initial value for the process lifetime and install the (shell, data) pointer pairs into the Rust style computation core alongside the other style metadata tables, so the core can select initial values without crossing the FFI. A parity test compares every table entry against property_initial_value on the C++ side, which is now exported from LibWeb for that purpose. The table is installed but not yet consumed; the driver switches over when longhand value selection moves into the core.
The property computation driver called back into C++ up to four times per longhand: to pin the winning cascaded value, to fetch the inherited value, to select the initial value, and to compute and store the result, with C++ then re-entering Rust three more times per longhand to decide whether the specified value must be kept for re-resolution when an ancestor changes. Move longhand value selection into the driver itself. The winning cascaded value comes straight from the cascaded property store, initial values come from the process-wide table installed by the previous commit, and inherited values come from a per-element parent snapshot of the inheritable computed values that C++ prepares in bulk before the drive. The importance and inheritance bitmaps, the raw cascaded font-size, and the viewport font-metric and shadow-root inheritance side effects accumulate in a results block that C++ applies once after the loop, replacing the C++ LonghandFlowState protocol entirely. The inheritance-dependence decision runs natively over the Rust value graph, with C++ consulted only for value kinds whose computational independence rule still lives with their shells. Explicit inherit of a non-inherited property fetches the parent value through a separately counted rare callback, since the snapshot only carries the inherited-by-default longhands. The one remaining per-longhand callback computes and stores the selected value. It also copies the parent's animated value for inherited properties at the same point in the flow as before, so that computation contexts built later in the loop still see animated inherited values; a new test pins that ordering by checking that a child's percentage line-height resolves against the parent's animated font-size. On the inheritance-heavy baseline workload this removes every per-longhand selection and query crossing: longhand callbacks drop from about 665 to about 334 per element and style value query entries from about 980 to about 4 per element, while the parent snapshot adds no style value allocations because inheritable keyword values are shared singletons.
Roughly 270 of the 333 longhands per element crossed the FFI only to store their selected value and run its bookkeeping, because the value itself requires no computation. Queue those store operations natively in the driver and flush the queue in one crossing before any callback that may read the stored values: the compute callback, the writing-mode query, and the end of the drive. The C++ side thus observes exactly the same store, animated-inheritance and bookkeeping sequence that one call per property used to produce, and the compute callback now only runs for properties that actually require computation. Batching extends how long the driver holds value shells: the fetched parent value of an explicitly inherited non-inherited property used to be consumed immediately, but can now sit in the queue until the next flush. The fetch pin therefore keeps every fetched value alive for the whole drive instead of only until the next fetch; with the shorter pin, the WPT ref tests that explicitly inherit freshly built grid track lists and overflow-clip-margin values crashed on released shells. A new test covers a batch of explicitly inherited non-inherited properties, including freshly built grid track lists. On the inheritance-heavy baseline workload, per-element longhand crossings drop from about 334 to about 42: 28 compute callbacks plus 14 batch flushes. The counters test now checks the compute and batch crossings together.
For properties without a dedicated computed-value rule, computation is absolutization of the specified value. Handle the two most common value shapes natively in the driver: value types whose absolutization is the identity (keywords that are not resolvable colors, numbers, integers, strings, custom identifiers, percentages, flex values, unicode ranges and URLs) queue directly into the store batch, and plain length values absolutize through the core's existing length resolution math against a per-kind cached resolution context fetched once from C++, entering the batch either unchanged or as a computed pixel length that the flush materializes. The C++ side installs a color keyword bitmap alongside the other style metadata tables so the core can tell which keywords resolve to something else at computed-value time, and hands out length resolution contexts through a new callback that mirrors the per-element context caching, flushing queued stores first since context construction reads stored values. Viewport dependency flags recorded during native absolutization travel through the driver results block. The remaining per-longhand compute crossings on the baseline workloads are almost exactly the properties with dedicated computed-value rules; cascaded keyword, percentage and length values on other properties now compute without crossing. A precision test pins that font-relative lengths absolutize to unquantized pixel values: the batch flush constructs the computed pixel length from the raw double exactly like the C++ absolutization path, without rounding through the CSSPixels fixed-point grid, observable through outline-offset which serializes its computed value directly.
Border and outline widths and letter- and word-spacing have dedicated computed-value rules whose logic already lives in the core as leaf functions the C++ dispatcher calls per value. Run them natively in the driver instead: the specified value absolutizes natively first (the identity for the line-width and normal keywords, the length resolution math for plain lengths), then the border-width snapping or the normal-to-zero spacing rule produces a pixel result that joins the store batch. Values the core cannot absolutize, such as calc, still fall back to the compute callback. The driver takes the device pixel ratio as an argument since the border-width snapping needs it, and the letter-or-word-spacing rule is now callable natively instead of only through its FFI wrapper. On the inheritance-heavy baseline workload this drops per-element longhand crossings from about 42 to about 24: 19 compute callbacks plus 5 batch flushes, with the remaining computes concentrated in the font cluster, line-height, and the list-valued rules.
Both rules already live in the core as leaf functions; run them natively in the driver. The store batch entry's pixel-length flag becomes a computed-value kind so the flush can materialize pixel lengths, integers, or superellipse corner shapes, keeping the C++ round-value cache for the common corner case. The inherited math-depth and math-style that the math-depth rule needs come straight from the parent snapshot as an integer and the compact keyword check, with the initial values applying when there is no inheritance parent. Superellipse-valued corner shapes and calc-valued math-depth still fall back to the compute callback, since their absolutization and resolution stay with C++ for now. Per-element longhand crossings on the inheritance-heavy baseline workload drop from about 24 to about 15: 11 compute callbacks plus 4 batch flushes, leaving the font cluster, line-height, and the list-valued rules.
Two moves take the last per-longhand crossings out of the property computation loop. First, the rules whose leaf logic already lives in the core now run natively in the driver: font-size, with the inherited size and math-depth read from the parent snapshot and the computed math-depth tracked across the loop; font-weight; font-style; font-width; line-height, resolving percentages against the font size carried by the line-height resolution context; the keyword forms of the font feature and variation settings; and single-name animation-name values. Font-family lists and the keyword-valued coordinated background lists are recognized as unchanged by computation, using the background-image layer count the driver records in passing. The computed writing-mode and direction are tracked natively as well, so logical alias pairing no longer queries C++. Second, values whose computation still lives in C++ no longer cross individually: the driver queues them in the store batch as compute-in-C++ entries, and the flush handler runs the dispatcher in entry order. Deferring computation into the batch preserves the previous semantics exactly, because C++ only ever reads stored values from inside callbacks the driver invokes after flushing. The per-longhand compute callback is deleted from the driver's callback table. On the inheritance-heavy baseline workload the longhand loop now crosses the FFI about once per element, a single batch flush, with per-value slow paths remaining only for the computational-independence decision of the deliberately C++-backed grid track lists. The remaining C++-computed entries, about seven per element, are counted separately as the porting backlog.
Start the port of ComputedValues group population into the Rust core with the inherited box group, whose five fields are computed keyword values and whose payload layout is already defined in Rust. The group builds from the computed style value data in one call, sharing instead of allocating whenever it can: the parent's payload when every field matches it, and the immortal default payload when every field holds its initial value. A value the core cannot map falls back to the C++ setters, which remain for the other groups. The group registry keeps the default payloads it hands to C++ so builders can share them, and payload retention mirrors the C++ side's intentionally-leaked sentinel handling. The main style path passes the inheritance parent into ComputedValues::create() so the builder can share payloads at construction time rather than only through the post-construction adoption pass; sharing with a parent whose inherited box is not the default is now covered by the group sharing test, along with a child override ending the sharing.
Same recipe as the inherited box group: the three table keywords map natively and border-spacing reads a computed pixel length, with the payload shared from the parent or the defaults whenever the values allow. Two-value border-spacing lists fall back to the C++ setters, since list children are only reachable through their shells for now.
Byte-wise Rust construction only works for the fully-plain groups whose layouts are defined in Rust. For the mixed groups, whose payloads hold C++ vectors, references and variants alongside plain fields, add a generic builder driven by per-group field descriptors that C++ registers once: each pokeable field carries its offset and kind (a keyword-mapped enum byte, a number, a pixel length, or an integer), enum fields carry a keyword-to-code table built on the C++ side from its own converters, and hard fields register as keyword constraints that let the constructor's initial value stand. The builder decodes every descriptor or returns null for the C++ population path, default-constructs a scratch payload through the group vtable, pokes the plain fields, and shares the parent or default payload when the result compares equal through the vtable's new field-wise equality callback; groups without a comparable layout report false and conservatively keep their payloads unshared. No group registers descriptors yet, so behavior is unchanged; groups switch over one table at a time.
The first group on the generic builder: twelve enum and number fields poke through registered descriptors, with the keyword-code tables built from the C++ converters at registration time, and the flex-basis and gap fields registering as keyword constraints so the constructor's initial values stand for their common forms. Compound alignment values and non-initial gaps fail descriptor decoding and fall back to the C++ setters. An i32 field kind joins the builder for order, which can be negative.
Color fields resolve against the element's own colors, which the group builder core cannot do, so the gathered value entries grow a resolved raw color that the C++ gather loop fills from its color resolution context, and a color field kind pokes it. The text reset group uses it for text-decoration-color, with the decoration line, thickness and white-space-trim registering as keyword constraints and the remaining fields as keyword-mapped enums. The constraint for text-decoration-line exposed that the group's constructor default held the none keyword's enum in a one-element vector, while the computed representation of none is the empty list; the payloads never compared equal, which also kept every element from sharing the group's default payload. The constructor default now matches the computed representation.
Opacity's normalization has not moved into the core, so the gathered value entries grow a resolved number that the C++ gather loop fills, mirroring the resolved-color arrangement, and a resolved-f32 field kind pokes it. The blend mode and isolation fields map as keyword enums, and the filters, box-shadow and clip register as keyword constraints. A pixel-length constraint kind joins the builder alongside, for fields like the scroll margins whose initial values are zero lengths rather than keywords.
The largest descriptor table so far: thirty-four descriptors cover the group's twenty-six fields, with four new field kinds rounding out the builder. A color-or-keyword kind lets outline-color's auto keyword leave the constructor default standing while resolvable colors poke through the gather-resolved path; an initial-value constraint compares the value data against the initial table by pointer identity, which holds exactly for untouched properties since the driver selects the table's entries directly; a non-negative pixel kind clamps outline-width the way its setter does; and a resolved-f64 kind carries shape-image-threshold's normalized number. Appearance feeds two descriptors from one property: the appearance field maps through a keyword table with the compatibility keywords excluded, so pages using them take the C++ path, while computed_appearance maps the raw keyword. The conditional setter sites keep their conditions and gain the adoption gate.
The largest inherited group brings two field kinds that unlock the remaining shell-bearing groups. A retained-shell kind pokes a style value shell into a single-pointer reference slot, retaining it through the core's existing shell reference bridge; the slot's constructor default must be null, and parent sharing falls out of pointer equality since inherited values and untouched initial values reference the same process-wide shells. A keyword-equality kind pokes derived booleans like whether -webkit-text-fill-color is currentcolor. The group builds right after the element's color is computed, resolving its color fields against a context copy that already carries that color, since the shared context only receives it further down. overflow-wrap maps through a hand-written converter matching the switch it replaces, having no generated one, and text-underline-position stays constraint-only since its compound forms have no single-keyword mapping.
The caret and accent colors pair a gather-resolved used color, poked at the nested offset inside their color-or-auto fields, with an auto keyword constraint on the computed part, since the used value resolves against the element's colors even when the computed value stays auto. A resolved-byte kind carries the used color-scheme, which depends on the page's preference rather than the value alone, alongside an initial-value constraint covering the scheme list fields. The color-scheme setters move from the top of create() down beside the group build: nothing in the function reads the group's scheme fields, since color resolution carries the scheme in its own context.
All six size fields register as keyword constraints, so the group adopts a shared payload when every size is untouched and falls back to the setters otherwise, until the core learns the size representation.
The transform list, the individual transform properties and perspective register as none-keyword constraints, the origins as initial-value constraints, and the box and style fields map as keyword enums. The group's seeded default payload keeps the comparisons sound.
The mask image and clip-path register as none-keyword constraints, the mask type as a keyword enum, and the seven coordinated mask layer properties as initial-value constraints, so the group adopts a shared payload whenever no masking applies and falls back to the setters otherwise. The url and image conditional setter sites keep their conditions inside the adoption gate.
Every grid field registers as an initial-value constraint, so the group adopts a shared payload for the overwhelmingly common case of an element with no grid properties, until the core learns the grid track and placement representations.
All twenty-one animation, timeline and transition properties register as initial-value constraints, so elements without any of them, the overwhelming majority, adopt a shared payload and skip the whole comma-list construction cluster.
The geometry properties register as initial-value and auto-keyword constraints, the stop and flood colors resolve through the gather, the opacities ride the resolved-number path, and vector-effect and shape-rendering map as keyword enums.
The paint fields, dash array and offset, and stroke-width register as constraints, the opacities ride the resolved-number path, miterlimit pokes as a number, and the rules, line joins, interpolation modes and text-anchor map as keyword enums, with clip-rule sharing fill-rule's table since the enums alias. The paint-order trio and the branchy stroke sites keep their conditions inside the adoption gate.
The list style type registers as an initial-value constraint since it spans counter styles and strings, the image and quotes as keyword constraints, and the position as a keyword enum.
Both groups are constraint-only: content, the counters, the anchor names and scopes, and the position-anchor family all register as keyword constraints, so elements using none of them, the common case, adopt shared payloads and skip the construction blocks entirely. The gather helper hoists above the first constraint-only builds, which run before any value resolution since they need none.
Float, clear, position, the overflows, box-sizing and resize map as keyword enums; aspect-ratio, z-index, vertical-align, containment, the container fields and will-change register as keyword constraints; and display registers as an initial-value constraint, which also pins the pre-transformation display since the box type transformation replaces the stored value whenever it applies.
The insets register as auto-keyword constraints, which also covers the anchor inset references since anchor values are not the auto keyword, and the margins and paddings as zero-pixel constraints, until the core learns the length box representation.
Each side pairs a gather-resolved border color and its retained shell with a none-style keyword constraint, which also pins the border data width at the constructor's zero per the used-width rule, while the computed width pokes separately since it survives the zeroing. The radii and border-image fields register as constraints and the corner shape parameters poke as resolved numbers, since the corner properties always carry computed superellipse values.
The background color pairs its gather-resolved color with the retained shell, and the image and every coordinated layer property register as initial-value constraints, which also pins the derived color clip and the default layer vector. Elements without backgrounds beyond a plain color, the common case, skip the whole layer construction. This is the last group on the descriptor machinery: the font group's population intentionally stays with C++, since its payload holds the platform font list and resists field-wise comparison, making it the coarse platform-resource boundary the style core is designed around.
The cascade previously ran as seven origin stage callbacks from the core into C++, each re-entering the core once per matched declaration block, with per-declaration callbacks for applicability and source slot assignment. C++ now collects every matched declaration block into one bulk description per element, tagged with origin, author context and layer indices, inline-style and whitelist-bypass flags, layer names and source identifiers, and the core derives the css-cascade-5 application order natively: normal user agent, user and presentational hint declarations, normal author declarations with inner shadow contexts first and layers in declaration order followed by the context's inline style, important author declarations with outer contexts first and layers reversed while preserving rule order within each layer and without layer names, then important user and user agent declarations. Source slots report back in one batch resolved through a per-block source table, the pseudo-element property check only crosses for elements with a pseudo-element, and unresolved-value parsing remains the counted parser slow path. The stage callback table, the per-declaration-block application entry point and the C++ stage and application helpers are deleted. On the baseline workloads the cascade drops from about seventeen crossings per recomputed element to two: the bulk entry and the source slot batch.
The calc tree normalizes subtraction and division into Negate and Invert nodes per css-values-4, and sum simplification folds same-unit leaves on their raw values, so CalcResult::add, subtract, divide_by and negate can never gain a caller; only multiply_by and invert are used by the product fold. CalcSerializer's resolve_as field was written at construction and never read. Delete all of it, along with the Calculated destructuring that only fed the dead field, and drop the CalcResult doc's reference to the C++ CalculationResult, which no longer exists. Most allow(dead_code) markers in the crate covered code that has since gained callers; strip the stale ones so rustc checks those items again. The markers that remain are load-bearing lint suppressions, not dead code: the selector FFI enums have variants only constructed by C++ across the boundary, and StyleValueData's payload fields are only read by C++ through the exposed layout, neither of which the lint can see.
📝 WalkthroughWalkthroughThe CSS style pipeline now performs bulk Rust-backed cascading and longhand computation, constructs and shares computed-style groups through FFI, exposes style-boundary counters, and adds metadata, CSS behavior, and baseline coverage. ChangesCSS style pipeline
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant DOM
participant StyleComputer
participant RustCascade
participant RustDriver
participant ComputedValues
DOM->>StyleComputer: update style
StyleComputer->>RustCascade: cascade matched declaration blocks
RustCascade-->>StyleComputer: cascaded properties and source slots
StyleComputer->>RustDriver: compute longhands with parent snapshot
RustDriver-->>StyleComputer: batched computed values and flags
StyleComputer->>ComputedValues: create computed values with parent
ComputedValues->>ComputedValues: adopt shared or newly built groups
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Meta/StyleFfiBaseline/collect.py`:
- Around line 22-29: Update Meta/StyleFfiBaseline/collect.py lines 22-29 and the
staged page imports in Meta/StyleFfiBaseline/animation-transition.html line 2,
custom-property-heavy.html line 2, inheritance-heavy.html line 2, and
shadow-slots-parts.html line 2 so the pages load include.js from their staged
location: use the deeper relative path, or change staging to preserve the
original location and document/support that choice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 161bffde-3bb1-45f8-a23e-115a95761d55
📒 Files selected for processing (42)
Libraries/LibWeb/CSS/ComputedValues.cppLibraries/LibWeb/CSS/ComputedValues.hLibraries/LibWeb/CSS/Rust/build.rsLibraries/LibWeb/CSS/Rust/src/calc.rsLibraries/LibWeb/CSS/Rust/src/cascaded_properties.rsLibraries/LibWeb/CSS/Rust/src/computed_values.rsLibraries/LibWeb/CSS/Rust/src/ffi_stats.rsLibraries/LibWeb/CSS/Rust/src/lib.rsLibraries/LibWeb/CSS/Rust/src/property_metadata.rsLibraries/LibWeb/CSS/Rust/src/selector_engine.rsLibraries/LibWeb/CSS/Rust/src/style_compute.rsLibraries/LibWeb/CSS/Rust/src/style_value.rsLibraries/LibWeb/CSS/RustStyleBridge.cppLibraries/LibWeb/CSS/RustStyleBridge.hLibraries/LibWeb/CSS/StyleComputer.cppLibraries/LibWeb/CSS/StyleComputer.hLibraries/LibWeb/CSS/StyleStructRef.hLibraries/LibWeb/CSS/StyleValues/RustStyleValueHandle.hLibraries/LibWeb/Internals/Internals.cppLibraries/LibWeb/Internals/Internals.hLibraries/LibWeb/Internals/Internals.idlMeta/Generators/generate_libweb_css_property_id.pyMeta/StyleFfiBaseline/animation-transition.htmlMeta/StyleFfiBaseline/collect.pyMeta/StyleFfiBaseline/custom-property-heavy.htmlMeta/StyleFfiBaseline/harness.jsMeta/StyleFfiBaseline/inheritance-heavy.htmlMeta/StyleFfiBaseline/shadow-slots-parts.htmlMeta/StyleFfiBaseline/single-element-restyle.htmlMeta/StyleFfiBaseline/small-document.htmlMeta/StyleFfiBaseline/stylesheet-heavy.htmlTests/LibWeb/TestStylePropertyMetadataParity.cppTests/LibWeb/Text/expected/css/animated-inherited-font-size-line-height.txtTests/LibWeb/Text/expected/css/computed-values-group-sharing.txtTests/LibWeb/Text/expected/css/em-length-computed-value-precision.txtTests/LibWeb/Text/expected/css/explicit-inherit-non-inherited-properties.txtTests/LibWeb/Text/expected/css/style-ffi-counters.txtTests/LibWeb/Text/input/css/animated-inherited-font-size-line-height.htmlTests/LibWeb/Text/input/css/computed-values-group-sharing.htmlTests/LibWeb/Text/input/css/em-length-computed-value-precision.htmlTests/LibWeb/Text/input/css/explicit-inherit-non-inherited-properties.htmlTests/LibWeb/Text/input/css/style-ffi-counters.html
💤 Files with no reviewable changes (1)
- Libraries/LibWeb/CSS/StyleComputer.h
This was referenced Jul 21, 2026
This was referenced Jul 29, 2026
This was referenced Aug 7, 2026
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.
Parts of the style system already run in Rust: selector matching, the cascade store, calc, style values and property computation. But C++ still drove them through fine-grained callbacks, so computing one element's style crossed the C++/Rust boundary constantly: once per declaration, per longhand, per style value queried, per
ComputedValuesfield. Those crossings are pure overhead, and they're also what keeps the style system from becoming a self-contained Rust component. This PR makes the loops themselves run in Rust, with the boundary reduced to a few bulk calls per element.To keep the work honest, the branch starts by adding simple counters for every crossing of the style FFI boundary (exposed as
internals.styleFfiCounters(), with fixed workload pages inMeta/StyleFfiBaseline/). This is temporary scaffolding to steer the port and catch regressions in coarseness, and it goes away once the port is done. On the inheritance-heavy workload we started at roughly 1330 crossings per element. Then, in order:math-depth, the font cluster,line-height) compute natively; anything Rust can't compute yet rides the same batch and C++ computes it at flush time, in the same order as before. The loop is now ~1 crossing per element.ComputedValuesbuilding: 22 of 23 style groups are now filled in by Rust from small per-field descriptor tables, including deciding when a group can share its parent's or the default payload. Only theFontgroup stays C++-populated, since it holds platform font resources that don't belong in the CSS core.!important, inline style, presentational hints) natively. Cascade crossings drop from ~17 to 2 per element.Since Rust now owns decisions that used to live in C++, the branch adds parity tests comparing every Rust-generated property table (inheritance, computation order, logical aliases, shorthands, initial values) against the C++ generator, plus regression tests for the edges the port exposed (animated inherited
font-size, explicitinheritof non-inherited properties, computed-value precision). Fulltest-webpasses at every commit, and a final commit deletes dead code found while auditing the crate's#[allow(dead_code)]markers.The point of all this: styling an element is now a Rust pipeline that C++ calls a handful of times, instead of a C++ pipeline that calls Rust thousands of times. What still crosses per-element is known and counted: selector matching's DOM lookups, custom properties, and the finishing/animation passes. As those close, full style computation becomes a Rust component with C++ only supplying fonts and consuming the result, which is what clears the way for styling the whole tree in parallel by default and, eventually, for a Rust layout engine reading Rust-owned style directly.