Skip to main content

Internals

How a <div> becomes lit pixels, and where the boundary is.

:::info These pages are generated from checked data

The pipeline stages, the shared-memory table roles and the guard list on these pages are read from guards/architecture/data.ts, which bun run arch:check validates against the repo: a cited file that moves, a guard script that gets renamed, or a shared table with no documented writer and reader all fail that run.

For the long-form argument behind each decision, see ARCHITECTURE-REVIEW.md — its Part 1 §4 is the authority on what a refactor must not touch. :::

The shape of it

Six hops, and five of them happen before the app ever runs.

  1. You write TSX and a stylesheet.
  2. Evaluate — importing the module runs the components, once. No renderer, no virtual DOM; the tree that comes back is the tree that gets compiled.
  3. Resolve — selectors match, specificity sorts, inheritance applies, shorthands expand, units convert. The answer is a set of numbers. This is the step a browser would redo every frame.
  4. Emit — the numbers are written out as a TypeScript module of typed arrays. Not serialized data that something will parse: the in-memory representation, already in memory.
  5. Write — past the boundary. Bun holds typed-array views over memory the engine allocated, so uploading is a store instruction rather than a call.
  6. Draw — Rust, Skia, Taffy.

Build time

  1. app.tsx + app.cssJSX, a stylesheet, and signals declared at module scope
  2. Evaluate, don't renderImporting the module is running the components — once, at build time
  3. Resolve the cascadeSelectors, specificity, inheritance and shorthands collapse to numbers
    Invariant: Resolve each pseudo-state as a full cascade, not a diff over the base. The merge story depends on it.
  4. Precompile the interaction statesEvery toggle and pseudo-state becomes a list of style-table writes
    Invariant: Patch the style table per (field, slot). Do not 'simplify' to swapping per-node style pointers — conflict detection and the predicate-mask table both depend on it.
  5. Map live objects back to exports`{count}` and `onClick={increment}` become named imports
  6. Emit app/ui.gen.tsTyped arrays, `satisfies CompiledUi` — the artifact is the IR

The boundary

  1. dlopen and describeThe engine allocates; Bun wraps each field span as a typed-array view
    Invariant: The arena stays a bare `*mut u8`, with slices materialised only inside function bodies. No Rust reference into shared memory may be live across a return to Bun.
  2. Write into the staged arenaA style patch is a memory write, not a call
    Invariant: Keep the staged/live/bounds split and span-wise commit. This — not monomorphism — is the real argument for struct-of-arrays. Do not collapse to one arena; do not go AoS.

Every frame

  1. tick()The one FFI call per frame
    Invariant: Keep the FFI boundary shape in full: catch_unwind, i32 status never a value, out-pointers, poisoning, and `panic = "unwind"` pinned in both Cargo profiles.
  2. Input, then commitSpan-by-span diff turns 'some bytes changed' into a narrow patch
  3. TaffyFlex and grid, rounded to whole pixels, bounds published back
    Invariant: Keep the systematic distrust of host-written table contents: budgeted walks, range-checked ids, and a bad string slot reading as "".
  4. SkiaRaster paint; an idle tick presents nothing at all
  5. Drain events → signalsA click writes a signal; batching makes it one repaint
    Invariant: Append-and-abandon list growth: no node id is ever invalidated, which is the only reason focus survives a reorder.

The frame phase runs on two threads: the engine thread owns the engine handle and services the OS, while the application runs in a Worker that writes the same engine memory. See Two threads — in particular why the engine thread may only try the lock, and why a missed commit is skipped rather than delayed.

The shared-memory tables

The boundary is memory, not a call surface. Struct-of-arrays, because the engine reads one field across every node at a time and a struct-of-structs layout would drag six unused fields into cache for each one.

The table definitions live in src/protocol/schema.ts and are imported by the architecture map directly, so the two cannot disagree. What the schema does not record is the direction of each table, which is what you actually need when debugging a wrong frame:

TableWritten byRead byNote
nodescompiler, then list relinking and `hidden`engineLink fields are prefilled to -1: zero is a valid node id, so zeroed memory would say every node is its own first child.
stylescompiler, then variant patchesengine, every frameStyle values stay zeroed, and there zero is real — `width: 0`, not auto. Auto is NaN.
variantscompilerengine painterPer interactive node: a bitmask of the predicates its styling reads, and where its style run begins.
mediacompilerengine, re-evaluated from the surface size each frameOne row per *atomic* condition, not per @media block, so the variant machinery resolves `and` for free as the combination where both bits are live. Thresholds are px — the engine never learns that rem exists. On the wire at all because a media query is the first styling input whose answer changes.
variantSlotscompilerengine painterEntry runStart+i is the style for the predicate combination whose compacted bits equal i; entry 0 is the base style.
listslist runtimeengineThe one place node count is a run-time value. Arenas grow by appending; ids are never reused.
layoutengineBun — hit-testing and the imperative APIThe only table that flows the other way.
stringsBun, incrementallyengineJS strings cannot be shared, so Bun writes UTF-8 into an arena and records (offset, length) here.

bun run protocol-guard proves the two halves still agree on offsets, field identity, enums and FFI symbols. bun run boundary-diff validates the tables Bun is about to hand over — link consistency, index ranges, sibling-chain cycles, arena bounds.

Further reading

  • The reactive rewrite — how count * 2 compiles.
  • Two threads — the engine thread, the app Worker, and the lock.
  • ARCHITECTURE-REVIEW.md — the fix-order authority.
  • bun run arch — the interactive map, with six animated figures.