A Load Problem Roots in the Prototype Player

Early versions of Figma’s prototype player kept things simple: load the entire document that contained the prototype into memory, then display the first screen. This approach worked well when documents were small, but scaling up exposed the flaw. Modern Figma files contain numerous pages and large design systems with many component variants. The effort required to pull an entire file’s worth of contents into memory caused load times to climb into minutes. On mobile, where memory budgets are tighter, the operating system would often kill the process before a prototype could finish loading.

The team’s core strategy to address this was incremental frame loading. Instead of fetching and holding an entire prototype in memory, the player now loads only the content needed to render what is currently on screen. This optimization reduces both initial load times and peak memory usage, because data is downloaded and stored only when it is necessary.

How the System Decides What to Load

The success of incremental loading depends on two big questions: how much of a prototype should load upfront, and when should more be loaded. For the player, time to interactive is the metric that matters most. To minimize that time, a client only needs the very first screen plus any adjacent screens reachable by a prototype interaction. Loading this initial subset unblocks interaction quickly. As the user navigates to a new frame, the client fetches that frame’s neighbors, maintaining the pattern throughout the session.

Making this work required changes to Figma’s multiplayer backend. Historically, each file was tied to a server-side datastore that synced an entire file to every client. To support loading a file’s subsets, the backend needed to handle queries for specific parts of the document tree and stream updates for just those parts. The resulting document syncing protocol works like this:

  1. A server session starts empty. The client sends a query message identifying the subtrees it wants.
  2. The server returns a reply message with a snapshot of the relevant subtree, confirming the subscription.
  3. After that initial snapshot, any further updates to the subscribed nodes arrive as changes messages.

Handling Dependencies and Navigation

Subscriptions are not strictly limited to the nodes a user sees. Many Figma objects contain properties inferred from other objects. For example, a component instance depends on its backing component, and style or variable consumers depend on their definitions. A client’s subscription therefore also includes dependency chains. If a document edit introduces a new dependency outside the subscriber’s set, the server sends a node created change. When a dependency goes away and falls outside the subscription, the server sends a node removed change so the client can evict the node from memory.

Changing subscriptions during navigation also demands care. If a viewer navigates away from a screen, the client unsubscribes from its nodes. Revisiting that screen later triggers a re-subscription. To avoid stale data when a node was edited or deleted in the meantime, the client always clears unsubscribed nodes from memory. The server sends a fresh snapshot whenever a request is repeated, ensuring accuracy even through complicated editing histories.

Balancing Memory Use with Responsiveness

The incremental loading strategy has consequences for navigation. When an initial view only contains the current frame and its immediate neighbors, the next navigation requires downloading a new screen. Two fast navigations in a row can present a loading spinner while the new frame is fetched. The unresolved problem: how to support smooth navigation without eventually downloading the entire prototype.

Figma’s solution splits behavior by platform. On desktop, where memory constraints are less severe, the background continues loading the rest of the prototype to keep navigation instant. Mobile devices prioritize memory, so the client loads only the frames within reach and evicts any screens that fall outside the current subscription.

Another issue appeared during testing when devices locked up during the heavy work of animation playback, metadata computation, and processing of upcoming screens. Because Figma runs within the largely single-threaded browser environment, offloading that work to a web worker wasn’t possible, as the state required for these tasks exists only on the main thread.

Performance work concentrated on easing this main-thread load. Each incremental file load was split into smaller chunks to be processed gradually in the background. Renderer metadata that wasn’t currently displayed—including layout and boolean operation results—is computed only when a view genuinely needs it. Fixes to other expensive code paths also reduced CPU lockup. The combination of these optimizations delivered measurable improvements in load time, and the mobile crash rate dropped noticeably once the system was live.