For the slowest 5% of page loads, we’ve seen a 33% decrease in load times.

Every engineering team wants its product to feel fast. Beyond giving users the best possible experience, how quickly something loads fundamentally shapes their first impressions. This is especially true for us; users often spend their whole workday in Figma, so incremental performance improvements that save seconds add up over the course of a workday. As Figma’s product teams build more features and users add more content to their files, our platform teams strive to uphold and improve performance. Our ideal load time trend is down-and-to-the-right: We want file load times to decrease over time even as files increase in size.

Performance should correspond to user-perceived complexity. If a user loads a page with only a few frames, Figma should be able to display their canvas almost instantly, even if the file has dozens of other pages with hundreds of frames each. By examining usage patterns, we learned that many users were treating files as projects—using one file to house all aspects of a workstream—so most didn’t even navigate to all pages in a single session. We realized that we could drastically improve load times and reduce memory by dynamically loading content as needed, rather than populating all content at once.

Dynamic loading as a performance optimization isn’t a new concept, but there were a few unique challenges specific to the structure of Figma’s browser-based files that we needed to work through. Broadly, a Figma file is a tree of nodes, in which each node is an interactable layer with properties. Importantly, certain nodes may reference nodes on other pages. This means that there can be a number of cross-page dependencies between nodes—from the more obvious to the very complex—that we had to consider.

In Figma’s data model, an instance contains a pointer to another node, the instance’s backing component, which might live on another page. We refer to this edge as a read dependency, in which the instance has a read dependency on the component. In order to render the instance correctly, the client must first download the component’s node.

Components are elements you can reuse across your designs. They help to create and manage consistent designs across projects. An instance is a linked copy of the component, automatically receiving any updates made to the component.

In addition to components and instances, many Figma features involve read dependencies. For example, Figma implements styles as user-invisible nodes. If a frame utilizes the “BrandPrimary” fill style #FFD966, in order to render the frame, the client must first download the corresponding style node. Variables are similar. If a node applies a “text-subheader” size variable to its font size, the client requires access to the variable node in order to resolve the correct raw value (e.g. 16) for the font size.

Past iterations of dynamic loading for view-only files and prototypes allowed us to work through these challenges. We created a dependency framework called QueryGraph—represented as an in-memory graph of edges between dependent nodes—the component of Figma’s multiplayer technology that keeps track of which part of a file to send to connected clients. QueryGraph and its graph of read dependencies were the foundation of faster file and prototype loads for view-only users. In each case, the unit of dynamic loading corresponded to the type of content that we render on initial file load:

By page in the Figma canvas: Instead of loading the entire file, Figma starts by only loading the selected page and loads additional pages on demand. Using QueryGraph, Figma’s multiplayer system sends the requested page to the client, along with any required read dependencies on other pages.

Instead of loading the entire file, Figma starts by only loading the selected page and loads additional pages on demand. Using QueryGraph, Figma’s multiplayer system sends the requested page to the client, along with any required read dependencies on other pages. By frame in prototypes: Figma loads dynamically by frame for prototype viewers, where it only shows one screen at a time. Figma also preloads frames that you can reach within a set number of transitions from your current state to prevent noticeable lag. Again, QueryGraph takes care of ensuring the client has all of the requisite parts of the Figma file and nothing more.

While viewers were reaping the benefits of dynamic loading, 70% of our daily file loads come from editable files. We wanted to build on our success with dynamically loading prototypes by frame and view-only files by page, extending that same loading logic to editable files. The challenge was ensuring that changes propagate correctly across unloaded content when an edit affects components on pages that haven't been loaded yet. To ship this optimization for everyone, we couldn’t simply reuse the existing logic for read-only clients. Enabling dynamic loading for editors required extending our dependency-tracking system to support a new kind of edge.

We've evolved loading for prototypes and view-only and editor Figma files to load dynamically by frame and page.

While viewing or rendering parts of a Figma file requires read dependencies, writing or editing parts of a file requires write dependencies; in many cases, a write dependency is the inverse of an existing read dependency. This is because Figma caches the result of expensive calculations like the text layout algorithm. If the style changes, Figma will run text layout again and cache the derived data result so that it’s always ready for rendering. For example, if a text style controls a text layer’s font, that’s a read dependency from the text layer to the style node. And the inverse is a write dependency: The style node has a write dependency on the text layer, indicating that in order to safely edit the style node, Figma requires the downstream text layer.

In Figma, styles and variables are implemented as user-invisible nodes.

This is critical for supporting dynamic loading for editors. When a user edits a part of a Figma file—like changing the text style—the client must have access to all of the downstream objects that require updating, like the cached glyph information of a text layer.

Here are a couple examples of write dependencies in Figma’s data model:

A component has write dependencies on all of its instances, which may live on other pages. When a user edits a component, Figma needs to propagate and update caches on the component’s instances. The same relationship is true of text styles and variables.

Auto layout means that editing one node’s size or position can change the size and position of another node as well. This is therefore also a write dependency. With components and instances, this auto layout write dependency might cross pages. (For example, an update to a component on one page can result in an instance resizing on another, which may in turn cause the instance’s siblings to resize or be rearranged in an auto layout frame.)

While we considered several different approaches to dynamic loading for editors, there was a clear frontrunner: write dependency computation, which would mean updating Figma’s multiplayer system to take write dependencies into account. This is analogous to what we did for view-only users and read dependencies, but view-only cases are inherently less risky. If the loading logic for view-only users is wrong, while viewers may see an incorrect file, file integrity itself isn’t at risk. But with editing use cases, if the loading logic doesn’t include the correct dependencies, there is a risk of data inconsistencies corrupting the file data. (Imagine making a change and never seeing that change reflected on another page!) Given the complexity of defining write dependencies, we considered two other alternative, simpler solutions as well: delayed editing and backfill, and data model overhaul.

This approach would entail loading the first page using the same logic as dynamic loading for viewers. Once the first page loads, the user would be able to pan, zoom, and inspect its contents. In the background, Figma would continue to load the full file as it does today. The catch is that the file would be view-only until the rest of the file downloaded, so there might be a delay between when a user wants to make an edit and when they could take that edit action. This could also introduce complex loading logic: We would need to continue loading pages in the background, and still be able to bump a page to the front of the loading queue if a user navigates to it. Being able to backfill such a large amount of data without introducing frame hitching or noticeable lag for the user would pose a challenge.

Write dependencies represent derived data. When a user edits a dependency, our current systems use a push-based model to propagate updates to other nodes, which then recompute their derived data caches. We considered migrating these systems to use a pull-based reactive model instead so we wouldn’t need to worry about preemptively downloading dependent nodes before a user edits a dependency. But this would be a huge undertaking to overhaul our systems in this way—we wanted to deliver load time and memory wins for our users quickly, and didn’t feel like this approach was feasible in our time frame.