If you’ve used Figma, you are probably familiar with the layers panel, which represents all your design elements in a nested hierarchical list.The layers panel was originally built nearly seven years ago, when files were smaller and Figma had fewer features. Today, it’s common to see files with tens of thousands of layers. We started to see sluggish interactions with the panel, especially for very large files; layer panel performance could also cause editor operations like dragging or typing to slow down.To make this core part of our UI fast and smooth again, we rebuilt the layers panel architecture from the ground up.The previous approachFigma files are trees of nodes, similar to HTML files. Each node has a set of properties (name, visibility, etc.) and a list of child nodes. To render the layers panel UI, we construct a large JavaScript object containing all the data about each node.In the original architecture, we would assemble (and, as layers changed, reassemble) this object through a single data-gathering pass. Starting from each top-level node on the current page, we’d gather a bundle of data about the node. Then, if the node was expanded (via the caret icon), we’d recurse into its children, gathering the same data about them, and recursing into their children. On large files with lots of nodes expanded in the panel, this led to substantial slowdowns, as we repeatedly regenerated the data for the layers panel.Ultimately, this architecture had two primary weaknesses:Computing too much: We were computing data for all the expanded nodes, even though only 20–30 rows are actually visible to the user on a typical screen.Computing too often: We weren’t doing much caching of incremental computations. Any change to the layers panel (such as expanding a node) essentially required fully recomputing everything from scratch, for every expanded node.Each of these issues had a distinct fix.Fix 1: Two-pass computationTo first fix the issue of computing too much, we split our data-gathering phase into two passes.In the first pass, we fetch only the ordered list of row IDs that make up the panel, leaving all other data for later. Note that even this list of IDs is non-trivial to compute. Some of the tricky parts include:Children of autolayout frames are displayed in reverse order.Certain node types, such as widgets and FigJam stickies, don’t show their children in the panel.Fixed/scrolling headers in prototyping frames split children into two subsections.Top-level frames and components are sticky.Even computing the list of row IDs requires resolving a complex web of product dependencies.In the second pass, we compute data for the nodes (names, icons, lock status, visibility, selection state, etc.). Having the IDs from the first pass allows us to only compute data only for the nodes that matter: that is, the nodes that are windowed.Windowing is a common technique in UI frameworks where only the items in a scrollable list that are visible on screen are rendered. The previous architecture used windowing, but because everything was computed in a single pass, it still gathered the full data for the nodes that aren’t visible on screen and aren’t rendered.By moving to a two-pass approach, data like names, icons, lock status, visibility, selection state, and so on are now only computed for a few dozen rows, whereas previously they would be computed for potentially hundreds of thousands of rows!Fix 2: Caching derived dataTo fix the second issue of computing too often, we turned to a platform-level primitive we’ve developed over the last couple of years: derived properties.Each Figma node exposes a set of fields, which work like mutable variables—they can be written to and read from by application code.However, many properties of nodes are not themselves fields, but are instead computed from fields and from other properties. As an example, consider a node’s absolute position: its offset relative to the (0, 0) point at the center of the canvas. Nodes don’t store this value directly. Instead they store their relative position: their offset from their parent’s position. This makes it easy to move and rotate large trees of nodes all at once.But how do we compute a node’s absolute position for rendering? We can derive this property via a computation like:Plain textSelf.AbsolutePosition = Parent.AbsolutePosition + Self.RelativePosition