Mejoramos el rendimiento del panel de capas


El panel de capas es el plano central de un archivo de Figma. Rediseñamos su arquitectura con nuevas estrategias de cálculo y almacenamiento en caché, lo que permitió que algunas de las interacciones fueran entre un 30 % y un 50 % más rápidas en los archivos más grandes y complejos.
Compartir Mejoramos el rendimiento del panel de capas
Ilustraciones de Chou Chia Yu
Si has usado Figma, probablemente conozcas el panel de capas, que representa todos los elementos del diseño en una lista jerárquica anidada.
El panel de capas se desarrolló originalmente hace casi siete años, cuando los archivos eran más pequeños y Figma ofrecía muchas menos funcionalidades. Hoy es habitual encontrar archivos con decenas de miles de capas. Empezamos a notar que las interacciones con el panel se volvían lentas, especialmente en archivos muy grandes. Además, el rendimiento del panel de capas también podía ralentizar operaciones del editor, como arrastrar elementos o escribir.
Para que esta parte fundamental de la UI volviera a ser rápida y fluida, reconstruimos por completo la arquitectura del panel de capas.
El enfoque anterior
Los archivos de Figma son árboles de nodos, similares a los archivos HTML. Cada nodo tiene un conjunto de propiedades (como el nombre o la visibilidad) y una lista de nodos secundarios. Para renderizar la UI del panel de capas, construimos un gran objeto de JavaScript que contiene toda la información de cada nodo.
En la arquitectura original, ese objeto se generaba —y se volvía a generar cada vez que cambiaban las capas— mediante un único recorrido para recopilar los datos. Comenzábamos por cada nodo de nivel superior de la página actual y reuníamos un conjunto de datos sobre ese nodo. Luego, si el nodo estaba expandido (mediante el ícono del cursor desplegable), recorríamos recursivamente sus nodos secundarios, recopilábamos la misma información sobre ellos y repetíamos el proceso con sus propios descendientes. En archivos grandes, con muchos nodos expandidos en el panel, este enfoque provocaba ralentizaciones importantes, ya que regenerábamos repetidamente toda la información necesaria para el panel de capas.
Con el tiempo, vimos que esta arquitectura presentaba dos problemas principales:
- Calculábamos demasiada información. Procesábamos datos de todos los nodos expandidos, aunque en una pantalla típica el usuario solo ve entre 20 y 30 filas.
- Calculábamos con demasiada frecuencia. Apenas reutilizábamos resultados previamente calculados. Cualquier cambio en el panel de capas (como expandir un nodo) obligaba, en la práctica, a recalcular todo desde cero para todos los nodos expandidos.
Cada uno de estos problemas requería una solución distinta.
Solución 1: cálculo en dos pasadas
Para resolver primero el problema del exceso de cálculos, dividimos la fase de recopilación de datos en dos pasadas.
En la primera pasada obtenemos únicamente la lista ordenada de los identificadores (ID) de las filas que componen el panel y dejamos el resto de la información para más adelante. Incluso calcular esa lista de ID no es una tarea sencilla. Entre los casos más complejos se encuentran los siguientes:
- Los elementos secundarios de los marcos con Auto Layout se muestran en orden inverso.
- Algunos tipos de nodos, como los widgets y las notas adhesivas de FigJam, no muestran sus elementos secundarios en el panel.
- En los marcos utilizados para prototipos, los encabezados fijos o desplazables dividen los elementos secundarios en dos subsecciones.
- Los marcos y los componentes de nivel superior permanecen fijos.

En la segunda pasada calculamos los datos de los nodos (nombres, íconos, estado de bloqueo, visibilidad, estado de selección, etc.). Contar con los ID obtenidos en la primera pasada nos permite calcular únicamente los datos de los nodos que realmente importan: aquellos que forman parte de la ventana visible (windowed).
El windowing es una técnica habitual en los frameworks de UI que consiste en renderizar únicamente los elementos de una lista desplazable que son visibles en pantalla. La arquitectura anterior ya utilizaba windowing, pero, como todo se calculaba en una única pasada, seguía recopilando todos los datos de los nodos que no eran visibles y que, de todos modos, no se renderizaban.
Gracias al enfoque de dos pasadas, datos como los nombres, los íconos, el estado de bloqueo, la visibilidad, el estado de selección y otros atributos ahora solo se calculan para unas pocas decenas de filas, cuando antes podían llegar a calcularse para cientos de miles.
Solución 2: almacenar en caché los datos derivados
Para resolver el segundo problema —calcular con demasiada frecuencia— recurrimos a un componente fundamental de la plataforma que desarrollamos durante los últimos años: las propiedades derivadas.
Cada nodo de Figma expone un conjunto de campos, que funcionan como variables mutables: el código de la aplicación puede escribir y leer sus valores.
Sin embargo, muchas propiedades de los nodos no son campos propiamente dichos, sino que se calculan a partir de esos campos y de otras propiedades. Tomemos como ejemplo la posición absoluta de un nodo: su desplazamiento con respecto al punto (0, 0) situado en el centro del lienzo. Los nodos no almacenan ese valor de forma directa. En su lugar, almacenan su posición relativa, es decir, el desplazamiento respecto de la posición de su nodo principal. Esto facilita mover y rotar grandes árboles de nodos de una sola vez.
Pero ¿cómo calculamos la posición absoluta de un nodo para poder renderizarlo? Podemos derivar esa propiedad mediante un cálculo como el siguiente:
Self.AbsolutePosition = Parent.AbsolutePosition + Self.RelativePosition
El componente de propiedades derivadas proporciona una forma estructurada de definir estas propiedades y mantenerlas actualizadas automáticamente, de manera similar a las fórmulas de una hoja de cálculo que hacen referencia a otras celdas. Al utilizar este sistema, obtenemos varias ventajas de forma automática:
- Cada propiedad derivada conoce de qué campos y de qué otras propiedades derivadas depende. Esa información se compila en una representación optimizada del grafo de dependencias.
- Existen varias políticas de almacenamiento en caché, con distintos equilibrios entre velocidad y uso de memoria.
- De forma predeterminada, las propiedades derivadas se calculan de manera diferida: solo se evalúan cuando alguien las consulta.
La estructura en árbol del panel de capas se adapta de forma natural al uso de propiedades derivadas.
Antes recalculábamos los datos de un nodo cada vez que cambiaba cualquier parte del árbol. Con las propiedades derivadas podemos modelar explícitamente la estructura del panel de capas y las dependencias que existen dentro de él, de modo que solo sea necesario actualizar las secciones relevantes del árbol. Además, podemos almacenar en caché de forma eficiente los cálculos correspondientes a cada subárbol.
Así, en la primera pasada de procesamiento, donde calculamos la lista ordenada de filas, expresamos la lógica de esta manera (simplificada para facilitar su comprensión):
Self.OrderedChildren =
If(Self.Expanded)
Children.FlatMap(Child => Child.OrderedChildren)
Else
[]La propiedad OrderedChildren de un nodo solo se invalida si cambia su estado de expansión o si cambia la propiedad OrderedChildren de alguno de sus nodos secundarios.
Tomemos como ejemplo un archivo con la siguiente estructura.
También evitamos parte del procesamiento gracias a la evaluación diferida (lazy) de las propiedades derivadas. Por ejemplo, OrderedChildren es una propiedad lazy. Si un nodo nunca se expande, su propiedad OrderedChildren nunca se consulta y, por lo tanto, nunca se calcula.
Imagina que expandes el marco B para mostrar sus elementos secundarios. Con el sistema anterior, empezábamos por el marco A y volvíamos a calcular toda la información, incluidos los datos de los 100 elementos secundarios del marco C. En cambio, al expresar estas relaciones mediante propiedades derivadas, solo es necesario recalcular la información correspondiente a los marcos A y B. En cuanto llegamos al marco C, podemos detenernos porque sabemos que podemos reutilizar el valor ya calculado.
Uso de memoria
Agregar mecanismos de almacenamiento en caché implica, inevitablemente, un mayor uso de memoria. Estábamos dispuestos a aceptar pequeños incrementos si eso se traducía en mejoras de rendimiento. Sin embargo, un consumo excesivo de memoria aumentaría la frecuencia de los errores por "falta de memoria", que resultan especialmente frustrantes para los usuarios.
Una implementación ingenua del almacenamiento en caché de OrderedChildren podría verse así:
Frame A: [B, C, D]
Frame B: [C, D]
Frame C: [D]
Frame D: []El costo de memoria sigue una suma de números triangulares. Sumar 1 + 2 + … + n da n(n + 1)/2.
Cada nivel del árbol necesita almacenar en caché los ID de todos sus descendientes. Esto genera un crecimiento en forma de suma triangular, lo que da lugar a un uso de memoria de O(n²). En árboles muy profundos, esto puede representar decenas de megabytes, algo que no resulta aceptable.
Una rope es una estructura de datos similar a un árbol que almacena secuencias en fragmentos más pequeños, de modo que las ediciones y concatenaciones puedan reutilizar partes ya existentes en lugar de reconstruir toda la secuencia.
En lugar de eso, implementamos el almacenamiento en caché de OrderedChildren mediante una lista recursiva, inspirada en una estructura de datos tipo rope. Esto permitió maximizar la reutilización estructural componiendo la información mediante punteros, en lugar de copiarla. Como resultado, el uso de memoria pasó a ser estrictamente proporcional a la cantidad de nodos únicos (O(n)), a cambio de un pequeño costo adicional al recorrer punteros durante las lecturas. En comparación con el prototipo inicial, esto redujo el uso de memoria hasta en un 99 %.


Impacto en el rendimiento
Como resultado de este trabajo, observamos mejoras significativas en el rendimiento. Interacciones críticas del panel de capas, como expandir o contraer filas o alternar el estado de visibilidad o bloqueo, pasaron a ser 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 el principal beneficiado, las mejoras también se extendieron al editor de diseño de Figma en su conjunto. Al reducir el trabajo innecesario, mejoramos el rendimiento general del renderizado, lo que se tradujo en una mayor tasa de fotogramas por segundo (FPS) y en menos fotogramas lentos. Como consecuencia, operaciones que antes resultaban pesadas —como escribir, arrastrar elementos o seleccionar colores en determinados archivos complejos— ahora se ejecutan de forma mucho más fluida.
Aún queda mucho por hacer y seguimos buscando nuevas formas de mejorar el rendimiento en toda la plataforma de Figma.
¡Estamos contratando ingenieros!
Conoce cómo es trabajar en Figma y consulta nuestras vacantes.
Gracias a Eli Fitch, Amy Shan, Russell McClellan, Josh Ferrell, Connor Smith, Elynn Lee y Alex Triana por sus contribuciones!


