Reactivity (and the Frame Loop)

A framework re-runs your code when state changes — a dependency graph you declare just by reading. The seam with the render loop: values that change every frame must never live in that graph, or sixty writes a second recruit every subscriber. The framework owns the shell; the loop owns the frame.

Core Idea

The graph above is not a diagram of a reactive system — it is one, watching itself. Three sliders are signals (state you can write), the middle column holds computeds (values derived from other values), and the right column holds effects (code that touches the world — here, painting the preview). Drag a slider and the write pulses down the edges, each dependent node flashes and counts its recomputation — and the branches that don’t depend on that slider stay dark. That selectivity is the entire pitch of reactivity: you never declared those edges. No subscription list, no wiring code. Each node discovered its own dependencies simply by reading other values while it ran. Read = subscribe. Write = re-run the readers. Everything a framework like Vue or React does with your UI is an industrial-strength elaboration of that one loop.

The Whole Trick Is in get

Here is a working reactive system — the same shape the instrument above runs. It is not pseudocode:

let active = null                        // the consumer currently running

function signal(value) {
  const subs = new Set()
  return {
    get value() {
      if (active) subs.add(active)       // ← the read IS the subscription
      return value
    },
    set value(next) {
      value = next
      for (const run of [...subs]) run() // ← the write re-runs every reader
    },
  }
}

function effect(fn) {
  const run = () => {
    active = run
    try { fn() } finally { active = null }
  }
  run()                                  // first run discovers the dependencies
}

function computed(fn) {
  const out = signal(undefined)
  effect(() => { out.value = fn() })     // a signal kept fresh by an effect
  return out
}

The load-bearing line is the innocent one: if (active) subs.add(active). While an effect runs, it announces itself in a global; any signal read during that window quietly adds the effect to its subscriber set. Dependency tracking is a side effect of evaluation — which is why the graph in the hero instrument could draw itself.

Vue 3 is this idea wearing a Proxy: it wraps your state object so that plain property accessstate.radius, no .value — runs a trap that does the track on get and the trigger on set. A Proxy is a programmable layer in front of the object machinery that prototypes describe — every property read walks through the trap before it walks the chain. And production systems add two refinements worth knowing: an equality guard (writing the same value triggers nothing) and batching — triggered effects are queued and flushed once, in a microtask, so ten writes in one handler cause one re-run instead of ten. Where exactly “after your code, before the next task” lands is precisely the microtask slot in the event loop.

The Other School: Re-render and Diff

React arrives at the same guarantee — UI stays consistent with state — from the opposite direction. There is no tracking and there are no proxies. setState doesn’t push a change through a graph; it schedules the component function to run again. The re-run produces a fresh description of the UI (the virtual DOM), React diffs it against the previous one, and commits only the difference to the real DOM. Where Vue asks “who read this value?”, React asks “what does the whole component look like now?” — fine-grained tracking versus wholesale re-render with a diff to make it affordable.

Both schools exist for the same reason: touching the real DOM is the expensive part. A committed DOM write is not just a memory write — it can invalidate style, layout, and paint, the machinery covered in the browser’s rendering pipeline. So both batch aggressively, and both amortize DOM writes down to the minimum. But neither is free: one pays in graph bookkeeping on every read and write, the other pays in re-running your render code and diffing its output. That cost is negligible at human frequency. The question is what happens at machine frequency.

The Seam Rule

Both panels animate the same orbiting mote. The right panel does what every instrument in this knowledge base does: mutate a plain object, draw, done — two writes, one paint, no audience. The left panel routes the mote’s position through the reactive system — position is a signal, and a crowd of subscribers depends on it, the way a real app accumulates watchers, computed chains, and component bindings over time. At rest they cost nothing. But position changes every frame, and a write recruits every subscriber: at 1,000 subscribers, sixty writes a second is over a hundred thousand recomputations per second that nobody asked for — spent before any pixel changes, all on the single thread your frame budget lives on.

That is the seam rule: per-frame values do not belong in reactive state. Positions, rotations, scroll progress, pointer coordinates, clocks, animation phases — anything that changes at 60 Hz should never be written where a dependency graph or a re-render scheduler is listening. The framework’s whole design assumes state changes at human frequency: a click, a keystroke, a fetch. Feed it machine-frequency writes and its cleverness — tracking, scheduling, diffing — turns into pure overhead, sixty times a second.

Who Owns the Frame

The cure is a division of territory, not a framework setting. The framework owns the shell: panels, HUD readouts, settings, route state — everything that changes when a person does something. The loop owns the frame: a plain rAF callback mutating plain objects — or better, preallocated buffers — that the reactive world never reads per-frame. The bridge between them is a mutable ref: the loop writes into it at 60 Hz and nobody re-runs; the shell reads it when a human-frequency event asks for a snapshot.

The bridge libraries institutionalize exactly this. react-three-fiber’s useFrame hands you a per-frame callback that runs outside React state entirely — you mutate the Three.js scene graph in place, and React neither knows nor cares that a frame happened. TresJS does the same on the Vue side. In both, the framework composes the scene declaratively, once, then steps aside; the render loop is imperative territory by design.

Every instrument in this knowledge base keeps the same discipline: each one is a plain rAF loop mutating its own state, and no framework ever sees a frame. That separation — not any rendering trick — is why pages full of live canvases stay light.

Connections

Reactivity is a scheduling story, so its edges run everywhere the thread does. The microtask flush that batches effect runs is a specific slot in the event loop, and a storm of per-frame recomputations is one more way to blow the frame budget from the CPU side — the same ledger cpu-gpu-bound teaches you to read. What a framework’s commit actually costs is a rendering-pipeline question: a DOM write is cheap until it invalidates layout. Vue’s proxies are a front door bolted onto the property-lookup machinery from prototypes. And the mutable-ref discipline — the set of things is fixed, only their state changes, written in place every frame — is the same shape as object pooling in garbage collection: both are ways of keeping the hot path invisible to expensive observers.