wandres.dev
PERFORMANCE I · Grabar y leer un perfil

Leer un flame chart: la forma dice más que los números

Qué representan exactamente el eje horizontal, el vertical y el color de un gráfico de llamas, las cinco siluetas reconocibles, y el error de interpretación que casi todo el mundo comete.

⏱ 18 min

El gráfico de llamas es la representación más densa de información de todas las DevTools y también la peor entendida, porque su forma invita a una lectura intuitiva que es incorrecta. La anchura no es importancia, la altura no es coste, y el color no es gravedad. Una vez que las tres correspondencias están claras, la silueta de un gráfico de llamas se lee como un diagrama y responde en diez segundos preguntas que de otra forma exigen leer cientos de nombres de función.

🎯 Al terminar esta lección sabrás
  • Explicar qué representan el eje horizontal, el eje vertical y el color en el gráfico.
  • Reconocer las cinco siluetas características y qué patrón de código produce cada una.
  • Distinguir el tiempo propio de una función del tiempo total de su subárbol.
  • Localizar el marco accionable subiendo desde la hoja del árbol.

Los tres ejes de información

El eje horizontal es el tiempo, y solo el tiempo. Cada caja empieza donde empezó esa llamada y termina donde terminó. La anchura de una caja es su duración total, incluyendo todo lo que llamó. Dos cajas contiguas no tienen relación entre sí más allá de haberse ejecutado una después de otra.

El eje vertical es la profundidad de la pila. Una caja debajo de otra significa “fue llamada por”. La profundidad no dice nada sobre el coste: una pila de cuarenta niveles puede durar un microsegundo y una caja solitaria de primer nivel puede durar dos segundos. Lo que sí dice la profundidad es cuánta indirección hay, y una pila muy profunda con muchas capas de librería explica por qué un trabajo aparentemente simple tarda.

El color agrupa por categoría de trabajo, no por gravedad. Script, estilo, disposición, pintado y sistema tienen colores distintos, y ese es todo el significado. Un bloque de un color intenso no es peor que uno de otro. Lo que sí es informativo es el reparto de colores: un gráfico monocromático de script y otro con franjas alternas de estilo y disposición son dos problemas distintos.

⚠️
Cuidado

Una caja ancha no significa que esa función sea lenta. Significa que esa función y todo lo que llamó duraron eso. La función culpable está más abajo, y muchas veces es una hoja pequeña que se repite. La distinción entre tiempo total y tiempo propio es la que separa una atribución correcta de una acusación injusta a la función que simplemente estaba en el camino.

Las cinco siluetas

La torre. Una pila alta y estrecha que se mantiene con la misma anchura durante varios niveles. Significa una cadena de llamadas donde cada una delega en la siguiente sin hacer casi nada por su cuenta. El coste está en la hoja del fondo. Es la forma típica de una llamada que atraviesa varias capas de abstracción para acabar en una operación cara.

La meseta. Una caja muy ancha en un nivel bajo, casi sin nada encima. Significa una función que hace el trabajo ella misma en lugar de delegarlo: un bucle grande, un análisis sintáctico, una serialización. Es la silueta más fácil de optimizar porque el culpable es evidente.

El peine. Decenas o cientos de cajas idénticas, estrechas, una detrás de otra en el mismo nivel. Significa una operación repetida muchas veces. El coste individual es despreciable y el agregado enorme. Es la silueta que el gráfico de llamas muestra mal y la vista de abajo arriba muestra bien, porque suma todas las apariciones.

La escalera. Cajas que descienden de nivel progresivamente, cada una empezando donde acaba la anterior. Es recursión o iteración con acumulación de pila. Con frecuencia indica un algoritmo con complejidad peor de la que se supone.

El bosque. Muchos árboles independientes, cada uno con su propia raíz, sin un patrón claro. Es lo que produce un perfil de una aplicación real bajo carga normal, y no indica nada por sí mismo. Si el gráfico parece un bosque, la respuesta no está en la forma sino en las vistas agregadas del panel inferior.

Tiempo propio frente a tiempo total

Esta es la distinción que hay que interiorizar para no perder tardes acusando al marco equivocado.

El tiempo total de una función es todo lo que transcurrió entre su entrada y su salida, incluidas las llamadas anidadas. Es la anchura de su caja.

El tiempo propio es lo que se ejecutó en su cuerpo sin contar las llamadas anidadas. Visualmente es la parte de su caja que no está tapada por cajas hijas.

Una función con un tiempo total de ochocientos milisegundos y un tiempo propio de dos no es lenta: es una función que espera a otras. Optimizarla es imposible porque no hace nada. La vista de abajo arriba ordena precisamente por tiempo propio, y por eso es la vista que encuentra al culpable real.

El fragmento siguiente ilustra la diferencia con un caso construido, y sirve para practicar la lectura sobre un perfil propio. Pégalo en la consola con el panel grabando.

// Genera tres siluetas reconocibles en el grafico de llamas
(() => {
  const quemar = ms => { const t = performance.now(); while (performance.now() - t < ms); };

  // 1. La torre: cadena de delegacion que acaba en una hoja cara
  const nivel4 = () => quemar(120);
  const nivel3 = () => nivel4();
  const nivel2 = () => nivel3();
  const nivel1 = () => nivel2();

  // 2. La meseta: una sola funcion que hace todo el trabajo
  const meseta = () => {
    let x = 0;
    for (let i = 0; i < 8e6; i++) x += Math.sqrt(i) % 7;
    return x;
  };

  // 3. El peine: una operacion barata repetida muchas veces
  const dienteDelPeine = i => { const t = performance.now(); while (performance.now() - t < 0.4) {} return i; };
  const peine = () => { for (let i = 0; i < 300; i++) dienteDelPeine(i); };

  console.time('torre');  nivel1();  console.timeEnd('torre');
  console.time('meseta'); meseta();  console.timeEnd('meseta');
  console.time('peine');  peine();   console.timeEnd('peine');
})();

Al leer el perfil resultante, comprueba las tres cosas: en la torre, el tiempo propio está concentrado en la hoja y los tres niveles superiores tienen tiempo propio casi cero. En la meseta, el tiempo propio y el total coinciden. Y en el peine, ninguna caja individual llama la atención mientras que la vista de abajo arriba muestra la función repetida en el primer puesto.

Subir hasta el marco accionable

Encontrar la función que consume tiempo es la mitad del trabajo. La otra mitad es encontrar el punto donde puedes intervenir, que casi nunca es el mismo sitio.

Si el tiempo propio está en una función interna del navegador —recálculo de estilo, análisis del HTML, decodificación de una imagen— no hay nada que optimizar ahí dentro. Lo accionable es quién la provocó y por qué, y eso está unos niveles más arriba.

Si el tiempo propio está en una función de una librería, tampoco vas a modificarla. Lo accionable es la llamada de tu código que la invoca, cómo la invoca y cuántas veces.

El procedimiento es subir nivel a nivel desde la hoja hasta encontrar el primer marco cuyo fichero esté bajo tu control, y hacerse tres preguntas sobre él: ¿esta llamada tenía que ocurrir? —muchas veces la mejor optimización es no hacerla—, ¿tenía que ocurrir ahora? —diferirla a un momento inactivo resuelve el problema sin tocar el coste— y ¿tenía que ocurrir tantas veces?.

Ese recorrido es mucho más rápido si la lista de ignorados está bien configurada, porque el panel colapsa los marcos de librería y deja visible tu código. Es la misma configuración de blackboxing que hace usable el depurador, y aquí hace usable el gráfico de llamas.

El gráfico de llamas miente por muestreo, y saber cómo miente evita perseguir fantasmas

Conviene saber cómo se construye este dibujo, porque su método de construcción produce dos artefactos concretos que se confunden con hallazgos. El perfilador de JavaScript no instrumenta cada llamada: eso multiplicaría por varias veces el coste de ejecución y cambiaría lo que se está midiendo. Lo que hace es muestrear la pila a intervalos regulares, del orden de una vez por cada fracción de milisegundo, y reconstruir las cajas asumiendo que entre dos muestras idénticas la pila no cambió. De ahí salen los dos artefactos. El primero: una función que se ejecuta muchas veces y muy rápido puede no aparecer en absoluto, si su duración es menor que el intervalo de muestreo y tiene la mala suerte de caer siempre entre muestras. Esto es exactamente el caso del peine llevado al extremo, y es la razón de que a veces el tiempo aparezca atribuido al padre sin ninguna caja hija que lo explique: la hija existió y no fue muestreada. El segundo: una caja puede parecer más larga de lo que fue, porque su anchura se redondea a los límites de las muestras que la contuvieron. En intervalos de unos pocos milisegundos, ese redondeo es una fracción significativa. Las tres consecuencias prácticas son claras. Una, no persigas diferencias pequeñas entre dos perfiles: si una función pasa de siete a nueve milisegundos, puede ser ruido de muestreo y no una regresión; los deltas menores de un orden de magnitud en tiempos pequeños no son datos. Dos, cuando una atribución te parezca imposible, desconfía del muestreo antes que de tu comprensión del código, y confírmala con instrumentación explícita: una medida con la API de rendimiento no está muestreada y no miente. Y tres, el trabajo que no es JavaScript —estilo, disposición, pintado— no viene del muestreador sino de instrumentación real del motor, así que esos bloques sí son exactos. Esa asimetría explica una experiencia común: los tiempos de disposición son reproducibles perfil tras perfil y los de script bailan.