Why Saving a File Is Hard in a Collaborative Editor
The obvious way to implement autosave is to serialize the in-memory representation of a document and write it to disk. When something goes wrong, restoring lost work is just a matter of opening the backup. In a single-user desktop application, that's the end of the story.
Figma is not that kind of application. Documents are stored as a tree of layers — the scenegraph — which serves as the central data structure. Users regularly create files that are tens of megabytes compressed and hundreds of megabytes in memory. Serializing the largest of those files takes seconds, and that's disqualifying on its own. The performance bar for autosave is much higher than for other features that need to read the scenegraph, such as the plugin sandbox, precisely because autosave would interrupt the user at arbitrary moments.
Even if serialization were optimized to take only 100ms, that periodic stutter would be unacceptable. The browser is effectively single-threaded for JavaScript and WASM — shared memory multithreading is barely supported and not a safe option. The typical workaround is to split work across multiple frames, but that opens another problem entirely: the user could keep editing the document while serialization is in progress.
The elegant answer would be an immutable scenegraph that serialization reads from. That would require rewriting nearly every bit of product code, since the scenegraph touches everything. Immutable structures also have real tradeoffs, including slower writes and higher memory use.
There's also a product-level problem. Files in Figma are cloud-based and can be edited by multiple people simultaneously. Replacing a file with a local backup could wipe out newer changes made by collaborators. Leaving the backup as a separate copy fails too — many files are the source of truth for shared assets like design system components.
Saving Deltas Instead of Documents
The alternative is to store only the changes made after the document went offline. Collaborative editing already tracks this kind of "delta" to know what needs to be synced with the server. Restoring work means applying that diff on top of the latest server version of the document.
The typical flow looks like this:
- User loads a document
- User goes offline
- Edits accumulate in an in-memory pending changes buffer
- The buffer is committed to disk on a regular interval
- The document closes unexpectedly
- User reloads the document
- Pending changes are deserialized and applied on top of the document
- Pending changes are uploaded to the server
For on-disk storage, the choice is IndexedDB. It supports database indices and transactional operations, which suits potentially large datasets that need to be accessed in small chunks with strong integrity guarantees.
Pending changes are stored per property, per file, per node or layer. This granularity balances storage overhead against redundant I/O. Coarser granularity would mean appending more metadata to each record; coarser still — storing all pending changes in a single object — would require rewriting the entire set on every edit. Storing per-node changes limits disk writes to only what changed between commits.
The Problem with Nonlinear History
Re-applying a stored diff to a linear history is straightforward — that's how undo-redo works. A single-user application could constrain autosave to that case: allow only one instance of a file, and require the user to accept or discard any autosaved changes immediately upon reopening.
A collaborative editor has no purely linear history. Every local edit creates an implicit branch, even when users aren't explicitly forking a document. In normal operation these transient branches merge within fractions of a second, so nobody notices. But autosave targets disconnections lasting minutes to hours, sometimes longer. The client can accumulate a large set of offline changes; the server can receive large changes from other collaborators in the meantime. The autosaved changes are now unmistakably a branch that split off when the user disconnected.
Applying the diff therefore requires merging onto the latest document version. Even with a single user, editing the same file from multiple tabs produces multiple sets of changes that must be applied sequentially.
When Merges Become Messy
Figma's data structures are built for concurrent editing, so merging diffs generally works. But the multiplayer system is designed for small, frequent conflicts where immediate visual feedback helps collaborators resolve clashes. It does not understand user intent, and for large-scale changes it may produce a mess instead of a clean merge.
Large conflicts are rare, and the product is optimized for the common case. Before and after applying an offline diff, Figma automatically creates a version history checkpoint — similar to a Git commit — so the user can easily revert the merge if something goes wrong.
A proper conflict-resolution interface would be ideal, but visually representing diffs for a 2D design document is still an unsolved problem in the industry. Building it as a separate project — while keeping autosave independent of it — has organizational advantages: less coordination overhead, incremental validation, and lower risk if one effort hits unexpected limitations that force a redesign.
Product decisions fill the gap where technical solutions don't exist yet. The file browser shows prominent UI prompting the user to restore their changes. This reduces the chance of server-side conflicts and signals that the feature is for crash recovery, not long-term local storage — it stores diffs, not whole files.
That prominence is deliberate, which makes it critical to avoid surfacing autosave changes when the user wouldn't expect to have lost work. A false alarm erodes trust in a recovery mechanism that only matters when something goes wrong.
Disk State Must Match Memory
Autosave works well when the tab simply closes: local changes sit on disk, then get uploaded and cleared on the next reconnect. But the more common flow is a long-lived tab that writes to disk, syncs to the server, and then needs to remove those changes locally.
That clearing step is where the invariant gets strict: the changes on disk must be an exact mirror of the pending changes in memory. Missing a change on disk invites data loss. Extra changes on disk are just as dangerous, though for subtler reasons.
A stale change isn’t merely redundant—it can be wrong. Consider a stored instruction like “set the height of nodeID=15 to 100.” If the user has since changed that height to 50, replaying the stale instruction on the next reconnect would overwrite the newer value. In linear history, a stale change might just no-op, but that’s still confusing. With branching history, it can actively clobber a recent edit. The checkpoint system makes such a mistake reversible, but the user experience is still one of “Figma lost my changes,” which defeats the purpose of autosave entirely.
Several paths lead to stale disk state. The first is inherent asynchrony. On reconnect, the tab sends pending changes and waits for an acknowledgment. Until that ack arrives, the document is considered unsaved and the tab is kept open. A change becomes stale on the server as soon as it arrives, but it’s only safe to clear from disk once the client has both the ack and has removed the change locally. So the “unsaved” state persists until both conditions are met.
There are also subtle bugs. In one case, changes never cleared from disk after a disconnect/reconnect cycle. The autosave pipeline uses a document change observer to track what needs upload or disk write. On reconnect, the client reapplies local offline changes on top of the server snapshot. If a local change happened to match the server’s latest version, the property setter would short-circuit as a no-op—skipping the observer notification entirely. The autosave system thus never learned the change should be dropped from disk. The fix was conservative: after reconnecting, erase all stored changes and re-serialize every pending change in a single IndexedDB transaction.
Essential Complexity
Much of the implementation difficulty here is accidental, tied to Figma’s specific architecture. Another system would hit different wrinkles. But the sheer number of these edge cases points to something essential: branching, which stems directly from multiplayer support.
Autosave’s difficulties are a symptom of a broader reality at Figma. A multiplayer engine can’t be a sealed black box; it’s a leaky abstraction. Making collaborative editing the core paradigm injects asynchronous behavior—like transient branches—into ordinary features, which then carry their own complexity. That’s the trade-off for a product that feels powerful and live.




