Améliorer les performances dans le panneau des calques


Le panneau des calques est le plan central d'un fichier Figma. Nous l'avons réarchitecturé avec de nouvelles stratégies de calcul et de mise en cache, ce qui a entraîné des interactions 30 à 50 % plus rapides dans certains des fichiers les plus volumineux et les plus complexes.
Partager Améliorer les performances dans le panneau des calques
Illustrations par Chou Chia Yu
Si vous utilisez Figma, vous connaissez probablement le panneau des calques, qui comprend tous vos éléments de design dans une liste hiérarchique imbriquée.
La conception initiale du panneau des calques remonte à près de sept ans – les fichiers étaient alors plus petits et Figma offrait moins de fonctionnalités. Aujourd'hui, les fichiers comportent souvent des dizaines de milliers de calques. Nous avons commencé à observer des interactions lentes avec le panneau, surtout dans le cas des fichiers très volumineux ; les performances du panneau des calques pouvaient également ralentir les opérations de l'éditeur telles que le glisser-déposer ou la saisie.
Pour que cette partie centrale de notre UI soit de nouveau rapide et fluide, nous avons revu de A à Z l'architecture du panneau des calques.
L'approche précédente
Les fichiers Figma sont des arbres de nœuds, à l'image des fichiers HTML. Chaque nœud possède un ensemble de propriétés (nom, visibilité, etc.) et une liste de nœuds enfants. Pour restituer l'interface du panneau des calques, nous concevons un vaste objet JavaScript contenant toutes les données relatives à chaque nœud.
Dans l'architecture originale, nous assemblions (et, à mesure que les calques changeaient, réassemblions) cet objet grâce à un processus unique de collecte de données. En partant de chaque nœud de niveau supérieur sur la page actuelle, nous rassemblions un ensemble de données sur le nœud. Ensuite, si le nœud était développé (via l'icône chevron), nous parcourions récursivement ses enfants, en récupérant les mêmes données sur eux, puis nous passions à leurs enfants. Dans le cas des fichiers volumineux comportant beaucoup de nœuds développés dans le panneau, il en résultait des ralentissements importants, car nous régénérions les données du panneau des calques à plusieurs reprises.
En fin de compte, cette architecture présentait deux faiblesses principales :
- Calculs trop nombreux : nous calculions des données pour tous les nœuds développés, même si l'utilisateur ne voyait réellement que 20 à 30 lignes sur un écran standard.
- Calculs trop fréquents : il y avait peu de mise en cache des calculs incrémentiels. Tout changement dans le panneau des calques (par exemple, le développement d'un nœud) nécessitait de tout recalculer à partir de zéro, pour chaque nœud développé.
Chacun de ces problèmes avait son propre correctif.
Correctif 1 : calcul en deux étapes
Pour résoudre d'abord le problème des calculs trop nombreux, nous avons scindé notre phase de collecte de données en deux étapes.
À la première étape, nous ne récupérons que la liste ordonnée des ID de lignes qui composent le panneau, en laissant toutes les autres données pour plus tard. Notez que même cette liste d'ID est complexe à calculer. Certains aspects délicats sont les suivants :
- Les enfants des frames à mise en page automatique sont affichés dans l'ordre inverse.
- Certains types de nœuds, tels que les widgets et les notes FigJam, n'affichent pas leurs enfants dans le panneau.
- Les en-têtes fixes/déroulants des frames de prototypage scindent les enfants en deux sous-sections.
- Les frames et les composants de niveau supérieur sont fixes.

À la seconde étape, nous calculons les données des nœuds (noms, icônes, statut de verrouillage, visibilité, état de sélection, etc.). Disposer des ID de la première étape nous permet de ne calculer les données que pour les nœuds qui comptent, c'est-à-dire, les nœuds visibles à l’écran.
Le rendu fenêtré est une technique courante dans les frameworks d'interface utilisateur où seuls les éléments d'une liste déroulante visibles à l'écran sont rendus. L'architecture précédente utilisait le rendu fenêtré, mais comme tout était calculé en une seule étape, elle rassemblait quand même toutes les données des nœuds non visibles à l'écran et non rendus.
Grâce à l'adoption d'une approche en deux étapes, les données telles que les noms, les icônes, le statut de verrouillage, la visibilité, l'état de sélection, etc., ne sont maintenant calculées que pour quelques dizaines de lignes, alors qu'auparavant elles devaient être calculées pour des centaines de milliers de lignes potentiellement !
Correctif 2 : mise en cache des données dérivées
Pour résoudre le problème des calculs trop fréquents, nous nous sommes tournés vers une primitive de plateforme que nous avons développée ces dernières années : les propriétés dérivées.
Chaque nœud Figma expose un ensemble de champs, qui fonctionnent comme des variables mutables, lesquelles peuvent être écrites et lues par le code de l'application.
Cependant, de nombreuses propriétés des nœuds ne sont pas des champs en tant que tels, mais sont plutôt calculées à partir de champs et d'autres propriétés. Par exemple, considérez la position absolue d'un nœud : son décalage par rapport au point (0, 0) au centre du plan de travail. Les nœuds ne stockent pas directement cette valeur. Ils stockent plutôt leur position relative : leur décalage par rapport à la position de leur parent. Il est ainsi facile de déplacer et de faire pivoter de grands arbres de nœuds d'un seul coup.
Mais comment calculer la position absolue d'un nœud à des fins de rendu ? Nous pouvons dériver cette propriété d'un calcul tel que le suivant :
Self.AbsolutePosition = Parent.AbsolutePosition + Self.RelativePosition
La primitive des propriétés dérivées offre une manière formelle de déclarer ces propriétés et de les mettre à jour automatiquement, à l'image des formules d'une feuille de calcul qui font référence à d'autres cellules. En utilisant le système, vous obtenez gratuitement des avantages intéressants :
- Chaque propriété dérivée sait de quels champs et propriétés dérivées elle dépend. Cela est compilé en une représentation graphique de dépendance optimisée.
- Il existe plusieurs politiques de mise en cache, avec des compromis entre vitesse et utilisation de la mémoire.
- Les propriétés dérivées sont par défaut paresseuses : elles ne sont calculées qu'une fois lues.
La structure arborescente du panneau des calques correspond naturellement aux propriétés dérivées.
Auparavant, nous recalculions les données des nœuds à chaque changement survenant dans l'arbre. Avec les propriétés dérivées, nous pouvons modéliser explicitement la structure du panneau des calques et les dépendances qu'elle contient, de sorte que seules les sous-sections pertinentes de l'arbre doivent être mises à jour. Nous pouvons aussi mettre efficacement en cache les calculs de panneau au niveau des sous-arbres.
Ainsi, à la première étape de données, où nous calculons la liste ordonnée des lignes, nous exprimons les choses de cette manière (simplifiée pour plus de clarté) :
Self.OrderedChildren =
If(Self.Expanded)
Children.FlatMap(Child => Child.OrderedChildren)
Else
[]La propriété OrderedChildren d'un nœud ne serait invalidée que si son état d'expansion change, ou si la propriété OrderedChildren de l'un de ses enfants change.
Par exemple, imaginez un fichier qui ressemble à ceci.
Nous évitons également certains calculs en utilisant la fonctionnalité de paresse des propriétés dérivées. Par exemple, OrderedChildren est une propriété paresseuse. Si un nœud n'est jamais développé, alors ses propriétés OrderedChildren ne seront jamais demandées et donc ne seront pas calculés.
Imaginez que vous développiez la frame B pour révéler ses enfants. Avec l'ancien système, nous commencions à la frame A et recalculions tout, y compris les informations sur les 100 enfants de la frame C. Cependant, en exprimant les choses sous la forme de propriétés dérivées, seules les informations sur les frames A et B doivent être calculées. Dès que nous atteignons la frame C, nous pouvons nous arrêter, car nous savons que nous pouvons réutiliser l'ancienne valeur.
Utilisation de la mémoire
Ajouter une mise en cache augmente inévitablement l'utilisation de la mémoire. Les augmentations mineures de la mémoire ne posaient pas de problèmes si elles amélioraient les performances. Mais utiliser trop de mémoire augmenterait le taux d'erreurs « mémoire insuffisante », des erreurs frustrantes lorsqu'elles se produisent.
Une implémentation naïve de la mise en cache OrderedChildren pourrait ressembler à ceci :
Frame A: [B, C, D]
Frame B: [C, D]
Frame C: [D]
Frame D: []Le coût de mémoire suit un schéma somme des nombres triangulaires. Ajouter 1 + 2 + … + n donne n(n + 1)/2.
Chaque niveau de l'arbre doit mettre en cache les ID de tous ses descendants. Cela crée une somme des nombres triangulaires, conduisant à une utilisation de la mémoire en O(n²). Dans les arbres très profonds, cela peut représenter des dizaines de mégaoctets, ce qui n'est pas acceptable.
Une corde est une méthode à l'image d'un arbre permettant de stocker des séquences en parties plus petites, de sorte que les modifications et les concaténations peuvent réutiliser les parties existantes au lieu de reconstruire toute la séquence.
À la place de ceci, nous avons mis en œuvre la mise en cache OrderedChildren en utilisant une liste récursive, similaire à une structure de données de type corde. Le partage structurel s'en ai trouvé maximisé grâce à la composition via des pointeurs plutôt qu'en copiant. L'utilisation de la mémoire est devenue strictement proportionnelle au nombre de nœuds uniques (O(n)), au coût mineur de traversées de pointeurs supplémentaires lors des lectures. Cela a réduit l'utilisation de la mémoire jusqu'à 99 % par rapport au prototype original.


Impact sur les performances
Suite à ce travail, nous avons observé des améliorations radicales des performances. La vitesse des interactions critiques avec le panneau des calques (par exemple, le développement/la réduction des lignes ou l'activation/la désactivation de l'état de visibilité/verrouillage) s'est accrue de 30 à 50 % avec certains des fichiers les plus volumineux et les plus complexes.
Bien que le panneau des calques lui-même ait connu les plus grands gains de performance, l'impact s'est étendu à l'ensemble de l'éditeur de conception Figma. En réduisant le travail inutile, nous avons également amélioré les performances globales de rendu, avec pour résultat un nombre d'images par seconde plus élevé et moins de frames lentes. Ainsi, les opérations qui semblaient auparavant lentes (par exemple, taper, faire glisser et sélectionner des couleurs dans certains fichiers complexes) sont désormais systématiquement fluides !
Il reste encore beaucoup de chemin à parcourir et nous cherchons toujours de nouvelles manières d'améliorer les performances dans Figma.
Nous recrutons des ingénieurs !
Découvrez les conditions de travail à Figma et consultez nos postes vacants.
Merci à Eli Fitch, Amy Shan, Russell McClellan, Josh Ferrell, Connor Smith, Elynn Lee, et Alex Triana pour leurs contributions !



