A from-scratch, memory-safe browser engine in Rust, built to reach qualitative core-browsing parity with Chromium/Gecko while being lean, fast, and agent-native. This spec describes the stack as it exists now. ~37k LOC across 16 crates.
- Own the whole browser surface to ship bleeding-edge human ergonomics and to make AI-agent browser automation first-class and in-process (not Playwright/CDP over a socket).
- Reuse the best production engines as libraries, wrapped behind our own traits, rather
than reimplement them: Servo's
stylo(CSS),taffy(flex/grid),html5ever(parsing),swash(text), Mozilla's SpiderMonkey viamozjs(JS). Hand-roll only where a lean Rust design beats the alternative (block/inline/float/table layout, the arena DOM, the paint pipeline, the agent surface). - Hard boundary: never patch a JS-engine's internals — only sanctioned FFI/embedding.
- Verifiable: a WPT-style parity harness compares element geometry to headless Chrome at ±3px and gates every change (72/72 probes across 30 pages today).
- Platform: Linux/X11 primary (wgpu present path verified headed at ~1288 fps);
headless
render/evalneed no display.
| Crate | LOC | Role |
|---|---|---|
engine/net |
2300 | HTTP(S) via hyper 1 + rustls (ring); redirects, streaming, charset sniff, content-decode, in-memory HTTP cache, optional adblock |
engine/html |
790 | HTML parse via html5ever → arena DOM (custom TreeSink); streaming + fragment parsing |
engine/dom |
1140 | Arena DOM: Vec<Node> + generational NodeId; flat tree (shadow DOM/slots); session history |
engine/css |
5340 | Cascade behind a StyleEngine trait: Stylo (default) or MinimalCascade (fallback); value parsing |
engine/layout |
3490 | From-scratch block/inline/float/table + unified taffy LayoutPartialTree for flex/grid |
engine/text |
1630 | swash shaping + rasterization; fontdb discovery; bidi; per-char fallback; WOFF2/WOFF1 decode; measure + shaped-run + glyph caches |
engine/paint |
765 | Display-list build + tiny-skia CPU raster; stacking (z), overflow clip, glyph blitting, damage detection |
engine/compositor |
525 | Frame-timing/telemetry policy + damage-rect model (not a layer compositor) |
engine/js |
3830 | SpiderMonkey via mozjs 0.18 (feature-gated): reflectors, event loop, DOM bindings, ES modules |
engine/a11y |
1220 | DOM → role+name accessibility tree with geometry; occlusion-aware hit-test; semantic diff |
engine/page |
1590 | The shared pipeline: bytes → DOM → style → layout → paint; subresource fetch; scripts |
shell |
4390 | winit+wgpu GPU window, browser chrome, event loop, tabs, find-in-page; headless render/eval CLI |
agent |
6620 | In-process AI-agent browser: observation, typed actions, forms, LLM backends, capability scoping |
bidi |
— | WebDriver-BiDi remote end (JSON-RPC/WebSocket) over the shared pipeline (standards-track, not CDP) |
store |
— | Local encrypted password store + origin-scoped autofill (audited crates, zero hand-rolled crypto) |
tests/wpt |
1140 | Parity harness (geometry vs headless Chrome) + reftests |
Key external deps (pinned): html5ever 0.39, cssparser 0.34, stylo 0.19,
taffy 0.12, swash 0.2, fontdb 0.23, unicode-bidi 0.3, mozjs 0.18, wgpu 27,
winit 0.30, tiny-skia 0.12, resvg 0.44, hyper 1 + hyper-rustls 0.27/rustls 0.23
(ring), lru 0.12, brotli-decompressor 5, selectors 0.39.
The shell's default is ["gui", "stylo"] — the full GPU window with the real Stylo
cascade. Cascade selection is a compile-time feature that swaps the StyleEngine
implementation behind a runtime catch_unwind fallback.
| Feature (crate) | Effect |
|---|---|
gui (shell) |
winit + wgpu window. Drop with --no-default-features for headless render/eval. |
stylo (shell→page→css) |
Real Stylo cascade (heavy build). Off → MinimalCascade. |
spidermonkey (shell→page→js) |
Link SpiderMonkey (mozjs jit + intl/ICU) → real JS. Off → scripts are a no-op. |
spidermonkey-noicu |
SpiderMonkey without ICU (smaller binary). |
adblock (net) |
Brave's adblock crate at the request layer. |
Runtime commands (manuk <sub>): browse [url|terms] (GUI), render <url> -o png,
eval <expr>, browse … --frames N (GPU frame stats).
URL ─▶ net::fetch_streaming ─▶ bytes
│ (in-memory HTTP cache; preload scanner warms subresources concurrently)
▼
html5ever streaming parse ──▶ arena Dom (first-paint snapshot when <body> opens)
▼
cascade_styles() ──▶ StyleMap (Stylo or MinimalCascade), viewport-aware (@media, vw/vh)
▼
layout_document() ──▶ LayoutBox fragment tree (block/inline/float/table + taffy flex/grid)
▼
[spidermonkey] run inline + external <script> against the layout snapshot; if the DOM
mutated → re-cascade + re-layout
▼
fetch_and_apply_stylesheets / images / @font-face (concurrent) → re-layout
▼
paint: DisplayList ──▶ tiny-skia Canvas ──▶ (shell) wgpu texture ──▶ present
Subresource fetches (images, external CSS, fonts) run concurrently (join_all) on the
shared pooled HTTP/2 client. Incremental relayout classifies damage: >= Reflow → full
relayout; == Repaint → paint-only fast path (apply_paint_only).
hyper 1+hyper-utillegacy client overhyper-rustls/rustls(ring, webpki-roots), HTTP/1.1 + HTTP/2, connection pooling/reuse, a process-global shared client.fetch(GET + redirects),fetch_streaming(chunked, drives first paint),request(arbitrary method/headers, no redirects) for API clients.- Body content-decoding (gzip/deflate/br) via
async-compression; WHATWG charset sniffing. - In-memory HTTP cache (RFC-9111 subset): fresh
GET 200withCache-Controlmax-age/s-maxageserved without a round-trip;no-store/private/no-cache/max-age=0bypass. (No Vary, conditional revalidation, heuristic freshness, or disk yet.) - Optional request-layer content blocking (
adblockfeature).
- Reuses
html5ever(spec-complete tokenizer + tree builder, incl. adoption agency, foster parenting) driven by a customArenaSink(TreeSink,Handle = NodeId) that builds directly into the arena DOM — no intermediateRcDom. - Streaming parser (
StreamParser) yields a first-paint snapshot before the tail arrives. - Context-aware fragment parsing (
parse_fragment_in) forinnerHTML, so table-scoped content (<tr>/<td>/<option>/<li>) survives. - Declarative shadow DOM (
<template shadowrootmode>) → real shadow roots.
- Arena:
nodes: Vec<Node>indexed byNodeId, with parent/child/sibling links asOption<NodeId>. NoRc/GC → cheap to share across passes, triviallySend. - Generational
NodeId: packsgeneration<<32 | indexinto ausize. A freed slot bumps its generation, so a stale handle to a reused slot failsis_alive(returnsNonefromelement()) instead of aliasing a new node. Generation-0 (never-reused) nodes are byte-identical to a bare index — JS reflectors'i32encoding stays valid. Free list +discard_subtreereclaim slots; no auto-free (parser reparenting + JSremoveChildre-insert), so reclamation is opt-in at proven-discard sites. - Flat tree (shadow host → shadow content;
<slot>→ assigned light-DOM nodes). - Double dirty-bit (
dirty+dirty_descendants) for incremental restyle/relayout. - Shared
SessionHistorymodel (used by shell, agent, BiDi).
Pluggable StyleEngine trait (cascade(&Dom, &[Stylesheet]) -> StyleMap). Two impls:
- Stylo (default). Servo's production style system (
stylo 0.19). Manuk implements the fullselectors::Elementmatcher +TElement/TNode/TDocument/TShadowRootDOM trait wall over the arena, builds aStylist+Device(real viewport → correct@media/vw/vh), matches author + UA + inlinestyle=declarations, cascades toComputedValues, and maps them onto our flatComputedStyle. Gets real selector matching,@media,var(),@layer, correct specificity/!important. Grid is enabled via thelayout.grid.enabledpref.vertical-alignis patched from MinimalCascade (no computed accessor in 0.19). 72/72 parity. Wrapped incatch_unwind(per-page fallback to MinimalCascade). - MinimalCascade (fallback,
--no-default-features). From-scratch cascade over a documented subset: type/class/id/attr/pseudo selectors + combinators, specificity,!important, inheritance, UA defaults,calc(),hsl(), viewport units. Also 72/72 on the harness; its real-world weakness is selector/@media/var completeness (why Stylo is default). ComputedStyleis a flat struct of resolved values: color/bg, font (family list, size, weight, style), text-align, white-space, line-height,Dimsizing (Auto/Px/Percent/Calc, % stored 0–100), box model,position+insets,z-index,overflow,box-sizing, float/ clear, flex (direction/wrap/grow/shrink/basis/justify/align/gap), grid (tracks + placement),transform(2D op list), table props.
Produces a LayoutBox fragment tree (rect border-box in absolute coords, background,
border, node, content: Block(children) | Inline(runs)).
- From-scratch block formatting (margin collapsing, BFC), inline/line-breaking, floats
(
FloatContext), and tables (CSS2 §17 separated model) — the parts Taffy can't do and that carry the parity gate. - Flex & grid via a unified
taffytree (Blitz model):engine/layout/taffy_tree.rsimplements taffy 0.12's low-level traits (TraversePartialTree,LayoutPartialTree,CacheTree,RoundTree,LayoutFlexboxContainer,LayoutGridContainer) over the arena DOM. A flex/grid container and its directly-nested flex/grid descendants are solved and placed in one tree (shared cache); block/inline/float/table children are Manuk-measured leaves (compute_leaf_layout→measure_intrinsic= shrink-to-fit width + content height).ComputedStyle → taffy::Stylemapping is a shared function; geometry is extracted back into theLayoutBoxtree without re-solving. - Intrinsic-size memoization per layout (keyed by
(node, avail-width)) — kills taffy's repeated-probe O(n²). - Positioned (
relative/absolute),z-indexeffective-layer map, overflow clip rects.
- Font discovery via
fontdb; system + aFALLBACK_FAMILIESlist (Noto CJK/Emoji/Symbols/ Arabic/…). Faces registered by an internalFaceId;@font-facefamilies aliased. - Shaping/rasterization via swash:
ShapeContext(kerning, ligatures, RTL),ScaleContext(subpixel raster in 4 quarter-pixel buckets, color glyphs COLR/CBDT). - Bidi reorder (
unicode-bidi) → visual runs → per-face segmentation → shape. - Web fonts: pure-Rust WOFF2 (
brotli-decompressor+ glyf/loca transform-v0 + hmtx transform-v1 reconstruction → sfnt) and WOFF1 (zlib) decode; raw sfnt passthrough. - Caches (LRU): measured widths, fully shaped runs (glyph ids + positions), rasterized
glyph bitmaps — all keyed on
(font, size, …), so paint/scroll re-use instead of re-shaping.
- Builds a
DisplayListofRect/Text/Imageitems grouped by stacking order (effective z), each carrying an optional overflow clip. - CPU rasterizer over tiny-skia (
CpuPainter): fills, borders, image blits (straight- alpha RGBA), glyph coverage blit + color-glyph source-over, rect masks for clipping. DisplayListderivesPartialEq+changed_since/damage_since(prev) -> Rect— the invalidation primitives.- SVG images rasterized via
resvg/usvg(engine/page).
compositor: frame-timing telemetry (FrameTimer, jank vs 60fps budget) and the damage-rect policy — not a layer compositor.- GPU present (shell): winit surface + a wgpu full-screen-triangle pipeline sampling one
persistent page texture.
uploadreuses the texture (re-created only on resize) and a row-level canvas diff uploads only changed rows (upload_damage) — a small change writes a small band, an unchanged frame nothing. Paint is coalesced to one per frame (input sets aneeds_paintflag; the paint happens inRedrawRequested). - Not yet: GPU spatial/scroll tree (uv-offset scroll) and a Vello backend behind a
Paintertrait — both need live-window verification / a large dep.
- Embeds SpiderMonkey via
mozjs 0.18(oneRuntimeper process, thread-local;rooted!; reserved-slot reflectors keyed byNodeId). Never patches SpiderMonkey internals. - HTML event loop: macrotask FIFO (
setTimeout) + microtask checkpoints draining hostqueueMicrotaskand SpiderMonkey's native promise-job queue to quiescence. - DOM bindings: Tier-0 globals (window/console/navigator), traversal getters + mutation,
getElementById/querySelector,value/checked, reflector identity cache, a realEventmodel (capture/bubble/preventDefault),getComputedStyle, ES modules (CompileModule/ModuleLink/ModuleEvaluate). - Honest gaps: some bindings still round-trip through
eval'd JS strings; no WebIDL codegen,MutationObserver, or native Promisefetchyet;setTimeoutignores the delay.
- Hand-rolled DOM →
A11yNodetree (role + accessible name) per HTML-AAM implicit roles + WAI-ARIA overrides + a pragmatic accname subset; attaches absolute geometry. - Occlusion-aware
hit_test: prefers the highest effective-zbox containing a point (aposition:fixed/high-z overlay wins a click), deepest-wins within a layer. - Agent-facing:
find/find_containingby role+name, flatrole "name" @(x,y)rendering, and a race-free semanticdiff(which role+name nodes appeared/disappeared between snapshots).
The differentiator: the controller is an in-process agent sharing engine/page, so it
skips the delta-serializer + dual node-ID machinery every CDP/WebDriver stack needs for a
process boundary.
AgentBrowserowns a livePage+ scroll + history. Consent-gatedHandoffmoves a live session between the human shell and the agent (keeps logged-in state, half-filled forms).- Stable handles: arena
NodeIdis the agent's handle —resolve_handleonce, thenactivate/type_into_handlerepeatedly (no per-call tree rebuild). - Typed actions:
BrowserAction(Click/ClickHandle/Type/TypeHandle/Submit/Navigate/ ScrollBy) +perform()— an allocation-light in-process API. - Readiness: synchronous snapshot (loaded/title/interactive-affordance count) read from the shared page — no network-idle heuristics.
- Semantic diffing:
observe_diff()returns what changed after an action. - Provenance / Action-Guard (E6): page-derived observations are provenance-tagged (prompt-injection fence); forms model + capability scoping.
- LLM backends (e.g. Groq), replay, translation, triage, concurrency helpers.
bidi: a standards-track WebDriver-BiDi remote end over the same pipeline.store: local encrypted password store + origin-scoped autofill.
- winit event loop; a chrome band (back/forward/reload, omnibox with search-template + bare-
term → search, suggestions), tabs, find-in-page (highlight + active-match), text-field focus
- caret, hand-cursor over links,
resolve_href(unwraps DDG/l/?uddg=redirects, protocol-relative).
- caret, hand-cursor over links,
- Shared Tokio runtime (warm connection pool). Page work routes through
engine/page; the shell only presents. Headlessrender/evalsubcommands need no display.
| Type | Shape | Notes |
|---|---|---|
NodeId(usize) |
generation<<32 | index |
generational; .index()/.generation()/is_alive |
ComputedStyle |
flat resolved-value struct | one per node; Dim = Auto/Px/Percent/Calc |
LayoutBox |
rect, background, border, node, content |
fragment tree; border-box absolute coords |
DisplayList |
stacking-ordered Rect/Text/Image + clips |
changed_since/damage_since |
ShapedRun |
glyphs: Vec<GlyphPos>, width, metrics |
cached per (font,size,text) |
A11yNode |
node, role, name, bbox, z, children |
occlusion-aware hit-test; diff |
- Parity harness (
manuk-wpt parity): lays out 30 pages and compares each probed element's border-box geometry to headless Chrome at ±3px (font-agnostic). Gate: currently 72/72 under both the Stylo and MinimalCascade cascades. Reftests + per-crate unit tests round it out; the whole workspace test suite is green. - Headed GPU present is measured with
browse … --frames N(≈1288 fps present here). GUI visual behavior (scroll rendering) can't be verified headlessly — those items are gated on a live window.
Invariants: never patch SpiderMonkey/mozjs internals; every change keeps the parity gate
green; the shell/agent share one engine/page; commits go to main.
Known gaps (documented, not faked): JS bindings partly via eval-strings; no
MutationObserver/native-fetch/WebIDL-codegen; setTimeout ignores delay; HTTP cache lacks
Vary/revalidation/disk; line-breaking is whitespace-based (no UAX#14); no inline SVG/MathML
layout (namespaces folded); no GPU layer compositor / uv-scroll or Vello backend; DOM arena
reclamation is opt-in (no GC); WOFF2 skips TrueType-collection and unknown table transforms.
These are the tracked frontier (docs/parity/PARITY-IMPLEMENTATION-V2.md,
docs/parity/RESEARCH-FINDINGS-V2.md), most needing heavy builds or live verification.