This repository has no description
bez docs stylo-risk.md
8.6 kB

The risk of depending on Stylo #

Answers the open question in roadmap.md "Whether the thesis holds at all" on Stylo, and tests the decision recorded in roadmap.md "Decisions" ("Stylo provides style"). Research as of September 2026.

Recommendation #

Depend on Stylo with an exact version pin and an adapter layer. The DOM-facing traits are stable enough and upstream is responsive. The risks are process-level — a thin publishing bench, monthly breaking releases, API changes driven by Gecko's needs, a Python build step, and a concurrency bug — and none of them justifies generating the cascade in the early phases.

API stability #

  • stylo has published 22 releases: 0.0.1 (2024-04), 0.1.0 (2024-10), then 0.2.0 through 0.21.0 between 2025-03 and 2026-09, roughly monthly since 0.9. Every release is a 0.x minor bump, which cargo treats as breaking — 21 breaking releases in 18 months. selectors moves in lockstep (0.40.0).
  • There is no semver or stability policy. Releases historically followed each rebase onto upstream; since May 2026 they are published when Servo needs one (servo/stylo#376), which also records accidental and out-of-order version numbers.
  • The core traits changed less than the version numbers suggest. Between 0.1.0 and 0.21.0 in style/dom.rs and selectors/tree.rs:
    • ensure_data / borrow_data / mutate_data moved from AtomicRef[Mut]<ElementData> to ElementDataRef / ElementDataMut (0.13).
    • unopaque and the before_ / after_ / marker_pseudo_element methods were removed.
    • rule_hash_target was renamed ultimate_originating_element.
    • get_attr(&LocalName, &Namespace) became required, and an ElementContext supertrait was added.
    • About ten new methods were added, all with defaults. selectors::Element is essentially unchanged.
  • Blitz's cost per upgrade, measured from its upgrade commits: 0.15 and 0.21 needed no source changes; 0.11 and 0.16–0.19 needed 10 lines or fewer; 0.14 about 200 lines in list layout; 0.13 about 470 lines, including a new data wrapper and a rewrite of the TElement data accessors; 0.20 needed a revert and re-land. Most breakage lands on code consuming ComputedValues, not on the DOM traits — the part of this project's boundary with Stylo that will grow.

Coupling to Servo and Gecko #

  • servo/stylo is a filtered mirror of the Firefox repository with Servo's patches rebased onto each sync. Each sync needs a batch of fixup commits to keep the Servo build compiling (about 20 in September 2026). Servo-mode patches are increasingly upstreamed into Firefox, including changes motivated by Blitz, but Gecko's needs drive the API.
  • default = ["servo"], and the build requires exactly one of servo or gecko. Servo mode pulls in stylo_atoms, web_atoms, string_cache, servo_arc, malloc_size_of, to_shmem, url, encoding_rs, and unconditionally rayon.
  • Constraints the roadmap did not record:
    • links = "servo_style_crate": only one Stylo version can exist in a dependency graph.
    • build.rs runs a Python 3 Mako code generator; a Rust port is only an experiment.
    • Many properties are pref-gated in Servo mode, and :has() is deliberately not parsed outside Gecko because its invalidation is Gecko-only (servo/stylo#456).
    • Embedder-facing changes remain open, such as RestyleDamage becoming an associated type and restructuring Device media data.
  • Concurrency: running Stylo in several threads of one process panics in ArcRefCell (servo/stylo#361, still open). Prefs and the thread pool are process-wide. This bears directly on parallel verifier workers.

Atom interoperability #

Stylo's LocalName is GenericAtomIdent<web_atoms::LocalNameStaticSet>, a newtype over the same interner markup5ever uses. Conversion is a zero-cost wrap or .0, provided the web_atoms versions match; Blitz pins html5ever for exactly that reason. This settles the last open Phase 0 mechanical question, confirmed by a compiling, running crate: crates/dom/src/style.rs wraps attribute atoms with GenericAtomIdent::new directly, with no re-interning step, and style_check (crates/dom/src/bin/style_check.rs) matched browsers' computed display/position/etc. on 2958 of 2958 comparable style values across the enumerated corpus.

Maintainer intent #

  • Publishing Stylo as a standalone crate is sanctioned by the Servo TSC but thinly staffed: the thread in servo/stylo#376 describes effectively one publisher.
  • Blitz's lead maintainer sits on the Servo TSC, owns the crate, and authored a large share of Stylo PRs. Blitz's cheap upgrades partly reflect breakage being absorbed upstream by that person; a project without that link pays more.
  • Only Servo itself has support commitments (monthly releases plus a half-yearly LTS since Servo 0.1.0 in April 2026). Stylo has no LTS.
  • Servo's contribution policy bans LLM-generated code, documentation, pull requests, issues and comments, and maintainers have pushed back on LLM-written Stylo issues. Upstream reports and patches from this project must be written by a person.

Other embedders #

Blitz has tracked every release from 0.1 to 0.21, usually within days, and is still in beta; Stylo panics reported by its users trace into invalidation. Most other reverse dependencies are Blitz forks or wrappers, or servo-* crates (about 13 of 30); the independent ones are early 0.x projects, and one PDF renderer sits about 11 months behind. "The majority are not Servo" is true only if Blitz derivatives count separately.

Mitigations #

  1. Adapter boundary. One crate is the only code that names style:: or selectors::. Outward it exposes spec-named computed accessors and the DOM hooks (dirty bits, element state, attributes). Nothing downstream holds Arc<ComputedValues> or Stylo value types; values convert at that boundary. This fences off the kind of breakage 0.13 and 0.14 caused.
  2. Pinning. Pin the Stylo crates with = together with web_atoms, html5ever, markup5ever and cssparser as one unit, with Cargo.lock committed. Upgrade on a schedule, one release behind Blitz so its upgrade commit serves as the migration guide, and gate each upgrade on the computed-style differential suite.
  3. Vendoring readiness. Keep a script that vendors the pinned crates onto a mirror of the tag, for supply safety and for patching (MPL-2.0 requires modified files to be published).
  4. Concurrency. One Stylo document per process for verifier parallelism, or sequential style threading, until servo/stylo#361 is fixed.
  5. Upstream engagement is human-written and budgeted as human time.

Fallback, in increasing cost #

flowchart LR
  pin["exact pin +<br/>adapter crate (now)"] --> freeze["freeze a<br/>vendored copy"]
  freeze --> fork["fork it"]
  freeze --> replace["replace the cascade,<br/>keep selectors + cssparser"]
  • Freeze at the last good pin with a vendored copy. Free now; upstream fixes stop and the atom and parser versions freeze with it.
  • Fork the vendored copy, owning the Python codegen and the Servo-mode fixups. About 5.3 MB of Rust, with Servo's own team spending around 20 fixups a month on syncing: realistic only if frozen.
  • Replace the cascade, keeping selectors and cssparser, both of which have been stable. Write or generate origin and importance ordering, specificity, inheritance, initial values and computed values for the Phase 0 property set: roughly 3–8k lines, weeks rather than months, at block-layout scope, then growing per feature (animations, custom properties, container queries and :has invalidation are each substantial). The main cost is lost attribution between style and layout failures, so this waits until the layout generator is trusted; the Phase 0 differential suite becomes the replacement's oracle. lightningcss parses only and does not change the estimate.

Sources #