Verbesserung der Leistung im Ebenen-Tab


Der Ebenen-Tab ist der zentrale Bauplan einer Figma-Datei. Wir haben ihn mit neuen Berechnungs- und Caching-Strategien neu strukturiert, was zu 30 bis 50 % schnelleren Interaktionen in einigen der größten und komplexesten Dateien führte.
Verbesserung der Leistung im Ebenen-Tab teilen
Illustrationen von Chou Chia Yu
Wenn du Figma bereits genutzt hast, bist du wahrscheinlich mit dem Ebenen-Tab vertraut, der alle Designelemente in einer verschachtelten hierarchischen Liste darstellt.
Der Ebenen-Tab wurde ursprünglich vor fast sieben Jahren entwickelt, als die Dateien noch kleiner waren und Figma weniger Funktionen hatte. Heutzutage sehen wir üblicherweise Dateien mit Zehntausenden von Ebenen. Es waren langsame Interaktionen mit dem Panel zu beobachten, insbesondere bei sehr großen Dateien. Die Leistung des Ebenen-Tabs konnte auch Editor-Operationen wie Ziehen oder Tippen verlangsamen.
Um diesen zentralen Teil unserer UI wieder schnell und flüssig zu machen, haben wir die Architektur des Ebenen-Tabs von Grund auf neu aufgebaut.
Der bisherige Ansatz
Figma-Dateien sind Bäume von Knoten, ähnlich wie HTML-Dateien. Jeder Knoten verfügt über eine Reihe von Eigenschaften (Name, Sichtbarkeit usw.) und eine Liste von untergeordneten Knoten. Um die UI für den Ebenen-Tab darzustellen, konstruieren wir ein großes JavaScript-Objekt mit allen Daten über jedem Knoten.
In der ursprünglichen Architektur würden wir dieses Objekt in einem einzigen Datenaufbereitungsdurchlauf zusammenstellen (und, wenn sich die Ebenen änderten, neu zusammenstellen). Beginnend bei jedem Knoten auf der obersten Ebene der aktuellen Seite würden wir ein Bündel von Daten über den Knoten sammeln. Dann, wenn der Knoten erweitert wurde (über das Caret-Symbol), würden wir für seine untergeordneten Elemente eine Rekursion durchführen, dieselben Daten über sie sammeln und für deren untergeordnete Elemente eine Rekursion durchführen. Bei großen Dateien mit vielen im Panel erweiterten Knoten führte dies zu erheblichen Verlangsamungen, da wir die Daten für das Ebenen-Tab wiederholt neu generierten.
Letztendlich hatte diese Architektur zwei Hauptschwächen:
- Zu viel Berechnung: Wir berechneten Daten für alle erweiterten Knoten, obwohl nur 20 bis 30 Zeilen auf einem typischen Bildschirm tatsächlich für die Benutzer*innen sichtbar sind.
- Zu häufiges Rechnen: Wir hatten nicht viel Zwischenspeicherung von inkrementellen Berechnungen durchgeführt. Jede Änderung im Ebenen-Tab (wie das Erweitern eines Knotens) erforderte im Wesentlichen eine vollständige Neuberechnung von allem, für jeden erweiterten Knoten.
Für jedes dieser Probleme gab es eine eigene Lösung.
Fix 1: Zwei-Durchläufe-Berechnung
Um zunächst das Problem des übermäßigen Rechnens zu beheben, haben wir unsere Datenerfassungsphase in zwei Durchläufe unterteilt.
Im ersten Durchlauf holen wir nur die geordnete Liste der Zeilen-IDs, die das Panel bilden, und lassen alle anderen Daten für später. Beachte, dass selbst diese Liste von IDs nicht trivial zu berechnen ist. Einige der kniffligen Teile umfassen:
- Untergeordnete Elemente von Autolayout-Frames werden in umgekehrter Reihenfolge angezeigt.
- Bestimmte Knotentypen, wie Widgets und FigJam-Notizen, zeigen ihre untergeordneten Elemente nicht im Panel an.
- Feste/Scrollende Header in Prototyping-Frames teilen die untergeordneten Elemente in zwei Unterabschnitte.
- Top-Level-Frames und -Komponenten sind fest.

Im zweiten Durchlauf berechnen wir Daten für die Knoten (Namen, Symbole, Sperrstatus, Sichtbarkeit, Auswahlzustand usw.). Da wir die IDs aus dem ersten Durchlauf haben, können wir Daten nur für die Knoten berechnen, die von Bedeutung sind: nämlich die Knoten, die in einem Fenster dargestellt werden.
Windowing ist eine gängige Technik in Benutzeroberflächen-Frameworks, bei der nur die Elemente in einer scrollbaren Liste gerendert werden, die auf dem Bildschirm sichtbar sind. Die vorherige Architektur verwendete Windowing, aber da alles in einem einzigen Durchlauf berechnet wurde, sammelte sie trotzdem die vollständigen Daten für die Knoten, die auf dem Bildschirm nicht sichtbar und nicht gerendert sind.
Durch den Wechsel zu einem Zwei-Durchlauf-Ansatz werden Daten wie Namen, Symbole, Sperrstatus, Sichtbarkeit, Auswahlstatus und so weiter nun nur für ein paar Dutzend Zeilen berechnet, während sie zuvor für potenziell Hunderttausende von Zeilen berechnet wurden!
Fix 2: Zwischenspeicherung abgeleiteter Daten
Um das zweite Problem des zu häufigen Rechnens zu beheben, haben wir auf ein Plattform-Level-Grundelement zurückgegriffen, das wir in den letzten Jahren entwickelt haben: abgeleitete Eigenschaften.
Jeder Figma-Knoten stellt eine Reihe von Feldern bereit, die wie veränderbare Variablen funktionieren – sie können vom Anwendungscode beschrieben und gelesen werden.
Viele Eigenschaften von Knoten sind nicht selbst Felder, sondern werden stattdessen aus Feldern und anderen Eigenschaften berechnet. Ein Beispiel dafür ist die absolute Position eines Knotens: seine Verschiebung relativ zum Punkt (0, 0) im Zentrum der Arbeitsfläche. Knoten speichern diesen Wert nicht direkt ab. Stattdessen speichern sie ihre relative Position: ihre Verschiebung von der Position ihres übergeordneten Elements. Dies erleichtert das gleichzeitige Bewegen und Drehen großer Knotenbäume.
Aber wie berechnen wir die absolute Position eines Knotens für das Rendering? Wir können diese Eigenschaft über eine Berechnung wie diese ableiten:
Self.AbsolutePosition = Parent.AbsolutePosition + Self.RelativePosition
Das Grundelement mit abgeleiteten Eigenschaften bietet eine formale Möglichkeit, diese Eigenschaften zu deklarieren und sie automatisch auf dem neuesten Stand zu halten, ähnlich wie Formeln in einer Tabelle, die auf andere Zellen verweisen. Durch die Verwendung des Systems erhält man einige Vorteile kostenlos:
- Jede abgeleitete Eigenschaft weiß, von welchen Feldern und abgeleiteten Eigenschaften sie abhängt. Dies wird in eine optimierte Darstellung eines Abhängigkeitsdiagramms kompiliert.
- Es gibt mehrere Cache-Richtlinien, die Kompromisse zwischen Geschwindigkeit und Speicherverbrauch darstellen.
- Abgeleitete Eigenschaften sind standardmäßig „faul“: Sie werden nur berechnet, wenn sie abgerufen werden.
Die Baumstruktur des Ebenen-Tabs passt natürlich zu abgeleiteten Eigenschaften.
Früher haben wir Knotendaten immer neu berechnet, wenn sich etwas im Baum geändert hat. Mit abgeleiteten Eigenschaften können wir die Struktur des Ebenen-Tabs und die Abhängigkeiten darin explizit modellieren, sodass nur die relevanten Unterabschnitte des Baums aktualisiert werden müssen. Wir können auch Panel-Berechnungen auf Unterbaumebene effizient zwischenspeichern.
Im ersten Datendurchlauf, wo wir die geordnete Liste von Zeilen berechnen, drücken wir Dinge wie folgt aus (vereinfacht zur Klarheit):
Self.OrderedChildren =
If(Self.Expanded)
Children.FlatMap(Child => Child.OrderedChildren)
Else
[]Die OrderedChildren-Eigenschaft eines Knotens wird nur ungültig, wenn sich ihr Erweiterungszustand ändert oder wenn sich die OrderedChildren eines der untergeordneten Elemente ändern.
Stell dir zum Beispiel eine Datei vor, die so aussieht.
Wir vermeiden auch einige Berechnungen durch die lazy-Funktion von abgeleiteten Eigenschaften. Zum Beispiel ist OrderedChildren eine lazy-Eigenschaft. Wenn ein Knoten nie erweitert wird, dann wird sein OrderedChildren nie angefordert und daher nicht berechnet.
Beispiel: Du erweiterst Frame B, um die untergeordneten Elemente sichtbar zu machen. Nach dem alten System würden wir bei Frame A beginnen und alles neu berechnen, einschließlich Informationen über alle 100 untergeordneten Elemente von Frame C. Indem wir Dinge als abgeleitete Eigenschaften ausdrücken, müssen jedoch nur Informationen über die Frames A und B berechnet werden. Sobald wir Frame C erreichen, können wir aufhören, weil wir wissen, dass wir den alten Wert wiederverwenden können.
Speichernutzung
Wenn Caching hinzugefügt wird, steigt der Speicherverbrauch unvermeidlich. Wir waren mit kleinen Speichersteigerungen einverstanden, wenn sie zu Leistungsverbesserungen führten. Aber die Verwendung von zu viel Speicher würde die Rate von „Nicht genügend Speicher“-Fehlern erhöhen, die frustrierend sind.
Eine naive Implementierung des Cachings von OrderedChildren könnte so aussehen:
Frame A: [B, C, D]
Frame B: [C, D]
Frame C: [D]
Frame D: []Die Speicherkosten folgen einer Summe der Dreieckszahlen. Das Hinzufügen von 1 + 2 + … + n ergibt n(n + 1)/2.
Jede Ebene des Baums muss die IDs all ihrer untergeordneten Elemente zwischenspeichern. Dies erzeugt eine Summe von Dreieckszahlen, was zu einer Speichernutzung von O(n²) führt. In sehr tiefen Bäumen können dies Dutzende von Megabytes sein, was nicht akzeptabel ist.
Ein Rope ist eine baumartige Methode, um Sequenzen in kleinere Stücke zu speichern, sodass Bearbeitungen und Verkettungen bestehende Stücke erneut verwenden können, anstatt die gesamte Sequenz neu zu erstellen.
Stattdessen haben wir das Caching von OrderedChildren unter Verwendung einer rekursiven Liste implementiert, ähnlich einer seilförmigen Datenstruktur (Rope-Datenstruktur). Dies maximierte die strukturelle Teilung durch Komposition über Zeiger anstelle des Kopieren. Die Speichernutzung wurde streng proportional zur Anzahl der eindeutigen Knoten (O(n)) entwickelt, mit geringen Kosten zusätzlicher Zeigerdurchläufe während des Lesens. Dies verringerte die Speichernutzung um bis zu 99 % im Vergleich zum ursprünglichen Prototypen.


Auswirkungen auf die Leistung
Als Ergebnis dieser Arbeit haben wir erhebliche Leistungsverbesserungen beobachtet. Kritische Interaktionen im Ebenen-Tab, wie das Erweitern/Zusammenklappen von Zeilen oder das Umschalten der Sichtbarkeit/Sperrzustand, wurden in einigen der größten und komplexesten Dateien um fast 30 bis 50 % schneller.
Während der Ebenen-Tab selbst die größten Leistungssteigerungen verzeichnete, erstreckte sich der Einfluss auf den gesamten Figma Design-Editor. Durch das Reduzieren unnötiger Arbeiten verbesserten wir auch die gesamte Rendering-Leistung, was zu höheren FPS und weniger langsamen Frames führte. Infolgedessen fühlen sich Operationen, die zuvor träge wirkten – wie Tippen, Ziehen und das Auswählen von Farben in bestimmten komplexen Dateien – jetzt durchweg reibungslos an!
Wir haben noch einen langen Weg vor uns und suchen immer nach Möglichkeiten, die Leistung in Figma zu verbessern.
Wir stellen technische Fachleute ein!
Erfahre mehr über das Leben bei Figma und sieh dir unsere offenen Stellen an.
Vielen Dank an Eli Fitch, Amy Shan, Russell McClellan, Josh Ferrell, Connor Smith, Elynn Lee und Alex Triana für ihre Beiträge!



