Saltar hasta el contenido principal

Mayor rendimiento en el panel de capas

Shannen WuSoftware Engineer, Figma
Peter HayesSoftware Engineer, Figma

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:

  1. 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.
  2. 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.
Diagrama de flujo dibujado a mano que muestra cómo las distintas condiciones de marco y dependencias determinan el comportamiento del panel de capas.Diagrama de flujo dibujado a mano que muestra cómo las distintas condiciones de marco y dependencias determinan el comportamiento del panel de capas.
Incluso calcular la lista de ID de filas requiere resolver una compleja red de dependencias del producto.

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.

“Windowing” significa que eliminamos lo que está fuera del área de visualización. Esta ilustración muestra una ventana amplia, pero la virtualización destaca cuando la parte recortada es minúscula.

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 redibujar solo las capas que cambian y mantener el resto estable, evitamos volver a calcular trabajos costosos con demasiada frecuencia.

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:

Plain text
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):

Plain text
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.

Antes, al expandir el marco B, se volvía a calcular todo el árbol a partir de esa página.
Ahora, al expandir el marco B, solo se vuelve a calcular el subárbol afectado (marcos A a B), mientras que el marco C y sus 100 elementos secundarios se omiten gracias a las propiedades derivadas.

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í:

Plain text
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.

Diagrama que compara la jerarquía del árbol del “enfoque original“ con las salidas agrupadas y simplificadas de los nodos B, D, C, E y F.Diagrama que compara la jerarquía del árbol del “enfoque original“ con las salidas agrupadas y simplificadas de los nodos B, D, C, E y F.
La memoria creció como una suma triangular, ya que cada nivel añadía otra copia de los datos de los niveles inferiores.
Diagrama que ilustra una representación de “estructura de cuerda” donde los nodos jerárquicos se corresponden con grupos segmentados y expandibles en el panel de capas.Diagrama que ilustra una representación de “estructura de cuerda” donde los nodos jerárquicos se corresponden con grupos segmentados y expandibles en el panel de capas.
El uso de una estructura tipo cuerda nos ayudó a volver a controlar el crecimiento de la memoria.

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!

Create and collaborate with Figma

Get started for free