Dynamic loading of real-time content at Figma¶
Summary¶
Figma extended its per-page dynamic loading system — already used by viewers and prototypes, which only read file contents — to editors, which also write. The core server-side data structure is QueryGraph, a bidirectional graph of both read dependencies and (newly added) write dependencies between nodes in a Figma file, held in memory by the Multiplayer service. On a page load the client names the initial page; the server computes the transitive closure of the page's nodes + their read + write deps and sends only that. As users edit and navigate, the server recomputes reachability and streams subscribed edits only. The hard correctness bar was editing parity: an edit made in a dynamically loaded file must produce exactly the same outcome as if the file were fully loaded. Figma ran Multiplayer in a shadow mode for an extended period — computing write dependencies alongside the still-full-load live path and reporting any edit to a node outside the computed write-dep set — to find missing dependencies (including a cross-page recursive constraint/instance one) before flipping the live path. Server-side decoding of the binary Figma file, now in the critical path because Multiplayer must decode before it can send a subset, was optimized by (a) preloading — the backend hints Multiplayer on the initial GET so decoding starts before the client's WebSocket connects (saves 300–500 ms p75) and (b) parallel decoding — persisted raw offsets let multiple CPU cores decode chunks concurrently (−40% decoding time on files that were previously >5 s serial). Client-side, instance sublayers are lazily materialized (derivable from the backing component + overrides), touching dozens of subsystems to remove the implicit "all nodes materialized at load" assumption. Six months of controlled A/B rollout; measured wins: 33% speed-up on the slowest / most complex file loads despite files growing 18% YoY, 70% reduction in nodes in client memory, 33% reduction in users hitting out-of-memory errors.
Key takeaways¶
-
Editing parity is a correctness contract, not a perf target. Figma names this explicitly: "Actions that users take in a dynamically loaded file should produce the exact same set of changes as they would if the file were fully loaded." A missing write dependency is not a latency bug; it's a silent data-divergence bug that shows up to users as instances diverging from their backing component, broken layout, or missing fonts. This is what forces write-dependency enumeration to be complete, not approximate, and justifies the multi-month shadow validation cost.
-
QueryGraph as a bidirectional server-side dependency index. Read deps were already explicit foreign-key fields on nodes (e.g. an instance's
componentIDpoints at its backing component). Write deps include implicit, non-foreign-key edges — the article's canonical example is auto layout: editing one node in an auto-layout frame can automatically change the layout of its neighbors, though none reference each other directly. These implicit write-deps become a new edge type in QueryGraph. The graph must be bidirectional because dynamic loading needs both sets for any given node: "When loading a file dynamically, it was important that we could quickly determine both sets of dependencies for a given node." (concepts/write-dependency-graph) -
Server-side subscription = reachability. Multiplayer holds both the full file and QueryGraph in memory. Each session has a subscribed set derived from the initial-page node + its transitive read+write dep closure. Edits from other users are sent to a session only if the edited node is reachable from that session's subscription. Changing a dependency edge can implicitly change another session's subscription: the cited example is swapping an instance to a new backing component — even if user A never touched the new component's descendants, multiplayer must recognize they are now reachable for user A's collaborator B and ship them. (concepts/reachability-based-subscription)
-
Shadow mode for graph correctness. Perfection of write-dep enumeration was validated by running Multiplayer in a shadow mode: track what page the user is actually on, compute write dependencies as if they'd loaded dynamically, but do not change runtime behavior; if an edit arrives for a node outside the computed write-dep set, report an error. This surfaced at least one "complex, cross-page, recursive write dependency involving frame constraints and instances" — the kind of edge case that would have been invisible without a production-shadow signal and would have caused user-visible layout bugs. The shadow phase exited only when the error stream was clean. (patterns/shadow-validation-dependency-graph)
-
Two server-side decoding optimizations paid for the critical-path insertion of Multiplayer. Previously the client could download the encoded file binary directly; dynamic loading made server-side decoding mandatory (the server has to understand structure to compute the subset). Two fixes:
- Preload-on-hint. On the initial page-load HTTP GET, the backend fires a hint to Multiplayer that a load is imminent. Multiplayer starts downloading + decoding the file before the client's WebSocket connection lands. Saves 300–500 ms at p75. (patterns/preload-on-request-hint)
-
Parallel decoding. Raw offsets are persisted in the Figma file format so decoding can be split into independent chunks processed concurrently by multiple CPU cores. Serial decoding exceeded 5 s for largest files; parallel decode reduces decoding time by >40%.
-
Client-side lazy materialization of derivable data. Instance sublayers (descendants of instance nodes) are fully derivable from the backing component + overrides. They were previously materialized eagerly on load. The dynamic-page-loading rework defers materialization for off-page instances, which required auditing and updating dozens of subsystems that had assumed "every node is materialized at load time." Analogous to concepts/lazy-hydration applied at the application data-model layer rather than a filesystem. The named cost is engineering breadth, not performance.
-
Trade-off Figma chose between three alternatives. The post names the three options considered and rejects two:
- Delayed editing + backfill (simpler, but wouldn't reduce client memory).
- Data-model overhaul to eliminate write deps entirely (cleanest, but too long-horizon to help users short-term).
-
Compute write deps and ship dynamic loading now — picked as the balance of performance win, feasibility, and user experience. This is a concrete instance of the concepts/simplicity-vs-velocity trade-off made explicit.
-
Measurable outcome. After a 6-month controlled A/B rollout:
- 33% speed-up on the slowest and most complex file loads (this is where big files break — the pool that matters most), despite files growing 18% YoY in size.
- 70% reduction in nodes in client memory.
- 33% reduction in users hitting out-of-memory errors.
Architectural shape¶
┌────────────────────────────────────────────────────────────────┐
│ Initial page-load HTTP GET │
│ backend ──(preload hint)──▶ Multiplayer │
│ │ │
│ ▼ │
│ download encoded file │
│ ┃ parallel decode │
│ ┃ (raw offsets → N CPU cores) │
│ ▼ │
│ [ full file in RAM ] │
│ [ QueryGraph in RAM ] │
│ ▲ │
│ │ read deps (FK: componentID, …) │
│ │ write deps (auto-layout, constraints │
│ │ instance↔component, │
│ │ cross-page recursive) │
│ client ──(WebSocket, "initial page = P")──▶ Multiplayer │
│ │ │
│ │ compute subscription_set(P) │
│ │ = closure(P, read∪write deps) │
│ ▼ │
│ send subset to client │
│ │
│ collaborator edit on node N ─▶ Multiplayer │
│ for each session S: │
│ if N reachable from S.subscription_set → ship edit │
│ if dep edge mutated → adjust S.subscription_set, │
│ ship newly-reachable nodes │
└────────────────────────────────────────────────────────────────┘
Caveats / not disclosed¶
- No numbers on QueryGraph memory cost on the server side for large files; the post notes Multiplayer holds "the full representation of the file and the QueryGraph" in memory but doesn't size either.
- No shadow-mode duration or error-rate trajectory named ("for an extended period of time").
- No preload-hint hit rate — fraction of loads that actually start before the WebSocket connects vs races lost.
- Parallel-decoding parallelism factor not given (>40% cut, but not "N-way over how many cores").
- Rollout was group-by-group over six months with A/B tests and telemetry; no specific incidents reported during rollout, but the post acknowledges monitoring "out of memory errors" as a primary telemetry signal.
- Editing-parity bugs prevented in production not quantified — only that the shadow framework "identified dependencies we had missed" and permitted fixes before flipping live behavior.
Raw file¶
raw/figma/2024-05-22-dynamic-loading-of-real-time-content-at-figma-e5d2e3e8.md- Raw duplicate (same URL re-fetched 2026-04-21, identical body):
raw/figma/2026-04-21-speeding-up-file-load-times-one-page-at-a-time-e5d2e3e8.md - Original URL: https://www.figma.com/blog/speeding-up-file-load-times-one-page-at-a-time/
- HN: https://news.ycombinator.com/item?id=40445746 (49 points)