Why component instances needed a new foundation

Figma's component system dates back nearly a decade, arriving only months after the product's first public release. The core concept remains straightforward: a main component defines the source of truth for properties and structure, while component instances act as synced copies that inherit any changes made upstream.

The original implementation relied on a runtime called Instance Updater—a self-contained system that handled property resolution, structure management, and synchronization with main components. Other systems, such as the layout engines, would delegate to Instance Updater whenever they encountered an instance.

Component instances can be deeply nested and highly customized. At runtime, Figma resolves overrides, swaps, and layout rules across the entire instance subtree. A small change can ripple through a surprisingly complex structure.

That insulated design was well-suited to 2016, when instance behavior was simple and Figma's runtime was far less complex. But a decade of feature additions changed the picture. Auto layout, variants, component properties, and variables each made design systems more expressive—and instances dramatically more intricate.

In a modern Figma file, an instance may combine variants, variable bindings, and auto layout. Its children might include nested instances with their own overrides, variable modes, and layout rules. Even small edits can propagate through the entire tree. As the team looked ahead to features like slots—now in open beta—it became clear the complexity would only keep growing.

Instance Updater kept receiving specialized, bespoke logic to accommodate new features. Each integration worked in isolation but grew increasingly fragile and slow as responsibilities accumulated. In some files, changing a single variable mode triggered a cascade of instance updates and layout recalculations across thousands of nodes. Worst-case scenarios saw different systems fighting over the same subtree, repeatedly invalidating each other's work. Operations such as swapping instances or changing properties could take seconds, locking up the editor entirely.

The team optimized where possible, but the underlying problem remained: a system designed for simple component copies was now responsible for coordinating layout, variables, and reactivity at massive scale.

An architectural reset

Incremental improvements were no longer sufficient. The team decided on a fundamental rethinking of how instances work, guided by three goals.

First, they wanted proper separation of concerns. Instance Updater had been performing its own auto layout and variable evaluation work for instances. The goal was to remove that complexity, letting layout systems handle layout and variable systems handle variable evaluation. The instance system would focus solely on resolving which properties and children an instance should have based on its main component.

Second, they needed granular invalidation. The old system updated entire instances whenever anything changed. In deeply nested components, this caused recursive cascades of work that froze the editor. The team wanted to update only the parts of the tree that actually changed.

Third, they recognized that features like reactive updates, dependency tracking, and efficient invalidation weren't unique to instances—other teams were asking for these capabilities too. This realization led to the most ambitious goal: building a generic framework any developer could use to create dynamic features.

Materializer and derived subtrees

The result is a system called Materializer. Unlike Instance Updater, Materializer is not specific to component instances. It's a generic system operating on Figma's document tree, responsible for creating and maintaining derived subtrees—subtrees whose structure and properties are computed from other sources of truth. Component instances are one example; rich text nodes that sync with external CMS content are another.

Rich text nodes are trees made up of text and image nodes that are generated based on the content defined by the rich text editor.

The key architectural insight was separating what to compute from how to keep it current. Materializer doesn't handle the business logic of what subtree to create. Instead, feature code defines a blueprint describing how a subtree should be derived from its source of truth.

For component instances, the blueprint defines how an instance resolves from its main component—which properties it inherits, which overrides apply, and which children should exist. For rich text, the blueprint takes rich text blocks (elements like <h1>, <p>, and <img>) as input and instructs Materializer to create corresponding text or image nodes. Materializer then tracks dependencies, invalidates stale data, and re-materializes only what's necessary.

With blueprints in place, the team turned to reactivity. Many independent changes can impact derived trees, and the system needed to avoid unnecessary work across the document on every update.

Two approaches were considered:

  • Pull-based invalidation checks whether a node is stale when reading it, similar to React. This avoids maintaining an explicit dependency graph but breaks down in Figma's environment. Cross-tree references and deeply nested dependencies mean that determining whether anything relevant changed often requires reconstructing large portions of the dependency chain on every read.
  • Push-based invalidation maintains an explicit dependency graph. When a source changes, its dependents are marked dirty and recomputed later. This requires more bookkeeping up front but provides precise control over what updates—and just as importantly, what doesn't.

The team chose push-based invalidation with one refinement: dependency tracking is automatic. 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.

Coordinating the runtime

Materializer alone wasn't enough. Figma's client has evolved into a collection of powerful runtime systems—layout engines, variable resolvers, and constraint enforcers—that each respond to document changes. These had evolved independently with their own logic for when and how to update.

Coordinating Materializer with these systems while preserving clear separation required a second step: a shared runtime orchestration layer. Before this work, each system managed its own updates and scheduled itself in ad hoc ways, making side effects difficult to reason about.

Unifying these runtimes under a common framework surfaced hidden feedback loops. Running updates in a predictable order revealed patterns where later systems would invalidate work done by earlier ones, forcing them to re-run. The team calls these back-dirties. Making them explicit helped eliminate many of them, moving toward a more unidirectional flow where source-of-truth changes trigger all relevant side effects predictably.

The payoff was a cleaner division of labor. Layout logic lives in layout systems. Variable logic lives in variable systems. Instance resolution no longer tries to coordinate everything itself. Product teams can own features vertically, building on shared infrastructure instead of re-solving the same orchestration problems.

Safely rebuilding core infrastructure

Rewriting instance resolution touched one of Figma's most heavily used code paths. Even small behavioral differences risked disrupting critical workflows across millions of files. While conceptually straightforward, porting instance resolution from a self-contained system into a shared runtime introduced subtle differences in execution order and behavior. Many of these weren't obvious and only surfaced in edge cases not yet covered by unit tests.

To manage the risk, the team relied on extensive side-by-side validation. For months, both the old and new runtimes ran in parallel across hundreds of thousands of real production files, comparing data models and rendered output to ensure designs looked and behaved identically.

The same validation process collected performance metrics and informed tradeoff decisions. Automatic reactivity required additional dependency caches, which improved interactive performance but carried potential costs in file load time and memory usage. Tracking these metrics alongside correctness allowed the team to identify regressions early and land targeted optimizations before rollout.

Only after the systems matched across correctness and performance did the team begin gradually shipping the new architecture to production. That discipline let them deliver a foundational rewrite while preserving the stability users rely on—with common operations in large design systems now up to 50% faster.

What the new foundation changed

The payoff from the rework showed up quickly in everyday workflows. In large design system files, operations like swapping instances and editing nested components ran noticeably faster. The root cause of the prior sluggishness was cascading reprocessing of entire instance subtrees; the new system avoids that unnecessary work.

Internal benchmarks showed one of the heaviest operations—variable mode changes—improving by 40–50% in large files. That case was a good indicator of the broader gains across common instance edits.

Editing large design systems has become noticeably faster!

Fewer classes of bugs

The strict separation between instance resolution, layout, and variable evaluation removed entire categories of subtle failures. Systems no longer contend for the same subtree, and feedback loops that previously caused oscillations are simpler to detect or avoid altogether. Because ownership is explicit—layout handles layout, variables handle variables, and instance resolution focuses on materialization—the behavior is far easier to reason about and safer to extend.

Faster feature development

The biggest return may be in developer velocity. Without this architectural foundation, adding new kinds of dynamic content would have meant reimplementing reactivity and invalidation from scratch. Rich text was the first net-new feature built on this foundation, and slots were created by composing on top of the new architecture rather than layering in another bespoke system. Other product teams at Figma are already using the Materializer framework to ship new kinds of dynamic content.

Slots rolled out earlier this month and was built on top of these new foundations.

This project reflects a philosophy of sweating the details: deeply understanding the problem space, designing clean abstractions, and executing with precision. The result is more than faster instances—it’s a durable foundation that will make future work at Figma quicker to ship.