Skip to content

CONCEPT Cited by 1 source

Push-based invalidation

Push-based invalidation is a reactive-update strategy: maintain an explicit dependency graph from sources to dependents; when a source value changes, mark all dependents dirty; recompute dirty nodes later (lazily at read time, or eagerly on a scheduled tick). The alternative, pull-based invalidation, has no explicit graph — on each read, the system walks the dependency chain to determine if the value is stale.

Figma articulates the choice explicitly in its 2026 Materializer retrospective (Source: sources/2026-04-21-figma-rebuilt-foundations-of-component-instances):

  • Pull-based — "check whether a node is stale when reading that node, similar to React. Avoids maintaining an explicit dependency graph, but breaks down in a system like Figma's. Due to cross-tree references and deeply nested dependencies, determining whether anything relevant changed often requires reconstructing large portions of the dependency chain on every read."
  • Push-based — "maintain an explicit dependency graph. When a source changes, mark its dependents as dirty and recompute them later. Requires more bookkeeping up front, but gives us precise control over what needs to update, and just as importantly, what doesn't."

The axis: where the cost lives

The choice is fundamentally where you pay for reactivity:

Axis Pull-based Push-based
Read path Expensive — walk chain, check staleness Cheap — read cached value
Write path Cheap — just update source Expensive — mark graph dependents
Graph storage Implicit / none Explicit, memory-resident
Fit for Reads ≪ writes, deps local Reads ≫ writes, deps spread
Bookkeeping cost Linear in read depth Linear in dependent count

Figma's workload — rendering + editing a document where every layer redraw reads many derived values, and deps cross pages / components / variables — lives squarely in "reads ≫ writes, deps spread." The read-path cost of walking reconstituted dep chains is the dominant term; push-based collapses that to a pointer dereference.

The refinement: automatic dependency tracking

Pure push-based requires someone to know the dep graph. The naïve approach — developers declare component.depends_on(main_component) by hand — has a known failure mode: the declared graph drifts from the actual reads in the code, producing silent staleness bugs.

Figma's refinement: the system records deps implicitly during materialization (see concepts/automatic-dependency-tracking). "As nodes read data during materialization, Materializer records those relationships implicitly. Developers don't declare dependencies by hand; the system learns them as it runs."

This refinement is what makes push-based practically correct at document scale. Without it, the dep-graph maintenance tax is paid per-feature and drifts; with it, the tax is paid once (in the framework) and is always in sync with code by construction.

Adjacent traditions

  • React — pull-based reconciliation (virtual DOM diff on each render). The reference point in Figma's post.
  • MobX / SolidJS / Vue refs / Svelte stores — push-based + auto dep tracking via proxied reads. Same refinement Materializer applies, scoped to app state rather than document tree.
  • Spreadsheet recalc (Excel) — explicit push (cells form a DAG, source edit → downstream dirty → recalc in topological order).
  • GitOps controllers (ArgoCD, Kubernetes controllers) — push via watch streams (etcd → controller); the controller is the "recompute on dirty" step.
  • Change-data-capture materialized views — dirty notifications delivered through a log; push with eventual consistency.
  • QueryGraph edit fan-out — push-based, but at document-node granularity for the collab network layer rather than derived-state granularity for the client runtime.

When pull-based still wins

The post doesn't enumerate these, but symmetric to the Figma case:

  • Extremely short dep chains — React's component tree is usually shallow enough that re-derivation on render beats push-tracking overhead on every useState write.
  • Memory-constrained runtimes — the explicit graph can dominate state size.
  • Static scope analyzable at build time — the framework can generate a closed-form recompute path and skip runtime graph maintenance (compile-time push).

Seen in

  • sources/2026-04-21-figma-rebuilt-foundations-of-component-instances — explicit trade-off articulation. Names React as the pull-based reference and cross-tree-references + deep-nesting as the workload reason Figma chose push.
  • sources/2026-04-21-figma-keeping-it-100x-with-real-time-data-at-scalesibling production instance at a different granularity: LiveGraph's server-tier cache is push-based at DB-row / query-shape granularity over a CDC stream, while Materializer is push-based at derived- subtree granularity on the client. Both refute the naïve pull-on- read default for the same reason (cross-cutting dependencies make pull walks expensive), but LiveGraph pays the push-side cost in a different coin: a stateless invalidator that computes affected queries from mutations via shape-argument substitution, rather than maintaining an in-process dep graph. The alternative Figma explicitly names (Asana's Worldstore) is an invalidation system that has to work differently because its query structure doesn't admit the same shortcut — a canonical "when query-shape push-invalidation doesn't apply" counterpoint.
Last updated · 200 distilled / 1,178 read