Mayor rendimiento en el panel de capas


El panel de capas es el elemento central de un archivo de Figma. Lo hemos rediseñado con nuevas estrategias de cálculo y almacenamiento en caché, con lo que las interacciones son entre un 30 % y un 50 % más rápidas en algunos de los archivos más grandes y complejos.
Compartir Mayor rendimiento en el panel de capas
Ilustraciones de Chou Chia Yu
Si has usado Figma, seguro que ya conoces el panel de capas, que muestra todos los elementos de tu diseño en una lista jerárquica anidada.
El panel de capas se creó hace casi siete años, cuando los archivos eran más pequeños y Figma tenía menos funciones. Hoy en día, es habitual ver archivos con decenas de miles de capas. Empezamos a notar que el panel respondía con lentitud, sobre todo con archivos muy grandes; además, el rendimiento del panel de capas podía ralentizar operaciones del editor como arrastrar o escribir.
Para que esta parte fundamental de nuestra IU vuelva a ser rápida y fluida, hemos rediseñado la arquitectura del panel de capas desde cero.
El enfoque anterior
Los archivos de Figma son árboles de nodos, similares a los archivos HTML. Cada nodo tiene un conjunto de propiedades (nombre, visibilidad, etc.) y una lista de nodos secundarios. Para mostrar la IU del panel de capas, creamos un objeto grande de JavaScript que contiene todos los datos sobre cada nodo.
En la arquitectura original, montábamos (y, a medida que cambiaban las capas, volvíamos a montar) este objeto mediante una única pasada de recopilación de datos. Empezando por cada nodo de nivel superior de la página actual, recopilábamos un conjunto de datos sobre ese nodo. Luego, si el nodo se expandía (mediante el icono del cursor), nos adentrábamos de forma recursiva en sus elementos secundarios, recopilando los mismos datos sobre ellos y repitiendo el proceso con sus elementos secundarios. En archivos grandes con muchos nodos expandidos en el panel, esto provocaba ralentizaciones importantes, ya que regenerábamos repetidamente los datos para el panel de capas.
Pero esta arquitectura tenía dos puntos débiles principales:
- Exceso de cálculos: estábamos procesando datos de todos los nodos desplegados, aunque en una pantalla normal el usuario solo ve entre 20 y 30 filas.
- Cálculos demasiado frecuentes: no almacenábamos en caché gran parte de los cálculos incrementales. Cualquier cambio en el panel de capas (como expandir un nodo) requería, básicamente, volver a calcularlo todo desde cero para cada nodo expandido.
Cada uno de estos problemas tenía una solución específica.
Solución 1: cálculos en dos pasadas
Para solucionar primero el problema del exceso de cálculos, dividimos nuestra fase de recopilación de datos en dos pasadas.
En la primera pasada, solo recuperamos la lista ordenada de ID de filas que componen el panel, dejando el resto de datos para más tarde. Ten en cuenta que incluso calcular esta lista de ID no es nada sencillo. Algunas de las partes más complicadas incluyen:
- Los elementos secundarios de los marcos con disposición automática se muestran en orden inverso.
- Ciertos tipos de nodos, como los widgets y las notas adhesivas de FigJam, no muestran sus elementos secundarios en el panel.
- Los encabezados fijos o desplazables en los marcos de la creación de prototipos dividen los elementos secundarios en dos subsecciones.
- Los marcos y componentes de nivel superior son fijos.

En la segunda pasada, calculamos los datos de los nodos (nombres, iconos, estado de bloqueo, visibilidad, estado de selección, etc.). Tener los ID de la primera pasada nos permite calcular datos solo para los nodos que importan: es decir, los nodos que están visibles.
La técnica de “windowing” es muy habitual en los marcos de trabajo de IU, donde solo se muestran los elementos de una lista desplazable que están visibles en pantalla. La arquitectura anterior utilizaba el “windowing”, pero como todo se calculaba en una sola pasada, seguía recopilando todos los datos de los nodos que no están visibles en pantalla y que no se representan.
Al pasar a un enfoque de dos pasadas, datos como los nombres, los iconos, el estado de bloqueo, la visibilidad, el estado de selección, etc., ahora solo se calculan para unas pocas docenas de filas, mientras que antes se calculaban para, potencialmente, cientos de miles de filas.
Solución 2: almacenamiento en caché de datos derivados
Al volver a dibujar solo las capas que cambian y mantener el resto estables, evitamos volver a calcular tareas costosas con demasiada frecuencia.
Cada nodo de Figma expone un conjunto de campos, que funcionan como variables mutables: el código de la aplicación puede escribir en ellos y leerlos.
Sin embargo, muchas propiedades de los nodos no son en sí mismas campos, sino que se calculan a partir de campos y de otras propiedades. Por ejemplo, consideremos la posición absoluta de un nodo: su desplazamiento respecto al punto (0, 0) situado en el centro del lienzo. Los nodos no almacenan este valor directamente. En su lugar, almacenan su posición relativa: su desplazamiento respecto a la posición de su nodo principal. Esto facilita mover y girar grandes árboles de nodos a la vez.
Pero, ¿cómo calculamos la posición absoluta de un nodo para el renderizado? Podemos obtener esta propiedad mediante un cálculo como este:
Self.AbsolutePosition = Parent.AbsolutePosition + Self.RelativePosition
La primitiva de propiedades derivadas ofrece una forma formal de declarar estas propiedades y mantenerlas actualizadas automáticamente, de forma similar a las fórmulas de una hoja de cálculo que hacen referencia a otras celdas. Al usar este sistema, obtienes algunas ventajas interesantes de forma gratuita:
- Cada propiedad derivada sabe de qué campos y propiedades derivadas depende. Esto se compila en una representación optimizada en forma de gráfico de dependencias.
- Hay varias políticas de almacenamiento en caché, con diferentes equilibrios entre velocidad y uso de memoria.
- Las propiedades derivadas son perezosas por defecto: solo se calcularán cuando se lean.
La estructura de árbol del panel de capas encaja perfectamente con las propiedades derivadas.
Antes, volvíamos a calcular los datos de los nodos cada vez que algo cambiaba en el árbol. Con las propiedades derivadas, podemos modelar explícitamente la estructura del panel de capas y las dependencias que hay en él, de modo que solo haya que actualizar las subsecciones relevantes del árbol. También podemos almacenar en caché los cálculos del panel a nivel de subárbol de forma eficiente.
Así que en la primera pasada de datos, donde calculamos la lista ordenada de filas, lo expresamos así (simplificado por claridad):
Self.OrderedChildren =
If(Self.Expanded)
Children.FlatMap(Child => Child.OrderedChildren)
Else
[]La propiedad OrderedChildren de un nodo solo se invalidaría si su estado de expansión cambia, o si cambia la propiedad OrderedChildren de cualquiera de sus elementos secundarios.
Por ejemplo, imagina un archivo que tenga este aspecto.
Además, evitamos algunos cálculos gracias a la característica perezosa de las propiedades derivadas. Por ejemplo, OrderedChildren es una propiedad perezosa. Si un nodo nunca se expande, entonces su OrderedChildren nunca se solicitará y, por lo tanto, no se calculará.
Imagina que ampliaras el marco B para revelar sus elementos secundarios. Con el sistema anterior, empezaríamos por el marco A y volveríamos a calcularlo todo, incluida la información sobre los 100 elementos secundarios del marco C. Sin embargo, al expresar las cosas como propiedades derivadas, solo hay que calcular la información sobre los marcos A y B. En cuanto llegamos al marco C, podemos parar, porque sabemos que podemos reutilizar el valor anterior.
Uso de memoria
Al activar el almacenamiento en caché, el uso de memoria aumenta inevitablemente. No nos importaba que hubiera pequeños aumentos en el uso de memoria si eso suponía una mejora en el rendimiento. Pero usar demasiada memoria aumentaría la frecuencia de los errores de “memoria insuficiente“, que son muy frustrantes cuando te los encuentras.
Una implementación sencilla del almacenamiento en caché de OrderedChildren podría ser algo así:
Frame A: [B, C, D]
Frame B: [C, D]
Frame C: [D]
Frame D: []El consumo de memoria sigue una suma de números triangulares. Si sumas 1 + 2 + … + n el resultado es n(n + 1)/2.
Cada nivel del árbol necesita almacenar en caché los ID de todos sus descendientes. Esto genera una suma de números triangulares, lo que conlleva un uso de memoria de O(n²). En árboles muy profundos, esto puede suponer decenas de megabytes, lo cual no es aceptable.
Una cuerda es una forma, parecida a un árbol, de almacenar secuencias en fragmentos más pequeños, de modo que, al editarlas o concatenarlas, se puedan reutilizar los fragmentos ya existentes en lugar de reconstruir toda la secuencia.
En su lugar, implementamos el almacenamiento en caché de OrderedChildren utilizando una lista recursiva, similar a una estructura de datos tipo cuerda. Esto maximizó el uso compartido de la estructura al componer mediante punteros en lugar de copias. El uso de memoria pasó a ser estrictamente proporcional al número de nodos únicos (O(n)), con el pequeño coste de tener que recorrer los punteros adicionales durante las lecturas. Esto redujo el uso de memoria hasta en un 99 % en comparación con el prototipo original.


Impacto en el rendimiento
Gracias a este trabajo, hemos notado mejoras drásticas en el rendimiento. Las interacciones clave con los paneles de capas, como expandir o contraer filas o cambiar el estado de visibilidad o bloqueo, se han vuelto entre un 30 % y un 50 % más rápidas en algunos de los archivos más grandes y complejos.
Aunque el panel de capas fue donde más se notó la mejora, el impacto se extendió a todo el editor de diseño de Figma. Al eliminar el trabajo innecesario, también hemos mejorado el rendimiento general de la renderización, lo que se traduce en más FPS y menos marcos lentos. Así que las acciones que antes se notaban un poco pesadas —como escribir, arrastrar y seleccionar colores en ciertos archivos complejos— ahora resultan fluidas en todo momento.
Todavía nos queda mucho por hacer, y siempre estamos buscando formas de mejorar el rendimiento en todo Figma.
¡Estamos contratando ingenieros!
Obtén más información sobre la vida en Figma y explora nuestros roles abiertos.
¡Gracias a Eli Fitch, Amy Shan, Russell McClellan, Josh Ferrell, Connor Smith, Elynn Lee y Alex Triana por sus contribuciones!


