Skip to content
Vidact

Under the hood

How compilation works

Follow a component from TSX through analysis to the direct DOM code that ships to the browser.

This page follows a component from parsed TSX through dependency analysis to generated JavaScript.

The pipeline

A module goes through five stages:

  1. Parse. The TypeScript and JSX are parsed with Oxc, and every identifier is resolved to the binding it refers to. Vidact never matches on names as text: useState is recognized because it is bound to the useState export of react, whether it was imported directly, aliased, or accessed as React.useState.
  2. Analyze. The component body is handed to the React Compiler's analysis, running natively in Rust. It produces a control-flow graph in SSA form, together with the facts Vidact needs: which values are reactive inputs, which expressions read them, where the Rules of Hooks would be violated, and where a value is mutated.
  3. Adapt. Those facts are translated into Vidact's own analysis types. The React Compiler's internal representation stops here; nothing downstream depends on it.
  4. Lower. The adapted facts become an updater IR: a list of sources (state, props, context, store snapshots), a list of updaters (things to do when a source changes), and for each updater the exact set of sources it reads and writes. Conditionals and lists become owned ranges, effects become effect updaters, and child components become prop updaters.
  5. Generate. The IR is printed as JavaScript for the chosen target: direct DOM construction for client, node claiming for hydrate, or string output for server. JSX is lowered in the same pass, and a source map back to the original TSX is composed.

A worked example

tsx
export function Greeting({ name }: { name: string }) {  const [excited, setExcited] = useState(false)  const punctuation = excited ? '!' : '.'  return (    <p onClick={() => setExcited(!excited)}>      Hello, {name}{punctuation}    </p>  )}

Analysis finds two sources: the name prop and the excited state. It finds one derivation, punctuation, that reads excited. It finds two bindings in the JSX: a text node reading name, and a text node reading punctuation.

Lowering turns that into three updaters, in dependency order:

UpdaterReadsWrites
Recompute punctuationexcitedpunctuation
Set text of node 1nameDOM
Set text of node 2punctuationDOM

Generation emits code that creates the <p> and its three text nodes, attaches the listener, and registers the three updaters with their read masks. When the click handler calls setExcited, the runtime marks excited dirty, runs the first updater, which marks punctuation dirty, then runs the third. The second never runs because name did not change.

What ships to the browser

The compiled module imports helpers from @vidact/runtime for element creation, text bindings, event batching, state slots, ownership, and scheduling. It creates DOM directly. The runtime keeps owner records and declared dependencies rather than a virtual element tree.

Feature families such as Suspense and transitions are separate entry points, so a module that does not use them never imports them.

Targets

The same IR is printed three ways:

  • client creates elements with document.createElement and inserts them.
  • hydrate walks existing DOM produced by the server target, claims each node, and attaches the same bindings and listeners. Mismatches are repaired and reported.
  • server walks the IR once and writes escaped HTML, inserting comment markers where the client will need to find range boundaries.

Server and hydrate output must come from the same compiler version, because the marker format is part of the protocol.

Dependencies

The Vite plugin runs the same pipeline on packages that ship React-shaped source. Each qualifying module is compiled per target with its source map chained to the published one, and the result is cached by content, target, features, and compiler version.

Why compile ahead of time?

Build-time analysis lets Vidact emit bindings tied to known source values. The runtime still handles scheduling, ownership, and DOM publication, but it does not need to re-run the component to discover its next output. This requires code the compiler can analyze, which limits the supported React subset.