wandres.dev
PERFORMANCE I · Grabar y leer un perfil

Encontrar la tarea larga y saber a quién pertenece

Localizar la tarea que bloquea el hilo, atribuirla a un origen concreto incluso cuando el código es de un tercero, y decidir entre las cuatro estrategias de arreglo.

⏱ 18 min

Una tarea larga es cualquier trabajo del hilo principal que supera los cincuenta milisegundos, y ese umbral no es arbitrario: es aproximadamente el punto a partir del cual la respuesta a una entrada del usuario deja de percibirse como inmediata. Encontrarlas en un perfil es fácil porque el panel las marca. Lo difícil, y lo que decide si el hallazgo sirve de algo, es atribuir cada una a un responsable concreto y elegir entre las cuatro cosas que se pueden hacer con ella.

🎯 Al terminar esta lección sabrás
  • Localizar las tareas largas de un perfil y ordenarlas por impacto real.
  • Atribuir una tarea larga a su origen, incluso cuando el marco superior es de un tercero.
  • Elegir entre eliminar, mover, partir o diferir según el tipo de trabajo.
  • Instalar un observador de tareas largas que funcione también en producción.

Localizarlas y ordenarlas

En la pista del hilo principal, las tareas que superan el umbral llevan un marcado visual en su borde superior. Con el rango completo seleccionado, esas marcas son un mapa de dónde mirar.

Pero la lista de tareas largas ordenada por duración no es la lista ordenada por importancia, y confundirlas es el error que hace que se optimice lo que no molesta. La ordenación correcta tiene en cuenta tres factores.

Cuándo ocurrió. Una tarea de trescientos milisegundos durante la carga inicial, mientras el usuario todavía está leyendo la primera pantalla, cuesta mucho menos que una de ciento veinte justo después de un clic.

Si había alguien esperando. Cruzar la pista de interacciones con la del hilo principal responde esto de un vistazo. Una tarea larga que solapa con una caja de interacción es la que hay que arreglar primero, siempre.

Con qué frecuencia se repite. Una tarea de ochenta milisegundos que ocurre una vez es un incidente; la misma tarea repitiéndose cada vez que el usuario teclea es un producto inusable.

De ahí sale una regla de priorización sencilla: primero las que solapan con interacciones, después las que se repiten, y por último las grandes y aisladas.

Atribuir la tarea

El panel dice qué funciones se ejecutaron. Traducir eso a un responsable requiere un poco de método, porque los tres casos habituales se resuelven de forma distinta.

Caso uno: el código es tuyo y se identifica. Con los source maps funcionando, el gráfico muestra tu fichero y tu función. No hay nada que atribuir: subes hasta el primer marco accionable y ahí está.

Caso dos: el código es de un tercero. El marco superior de la tarea pertenece a un dominio que no controlas. La información que necesitas no es qué hace ese código sino quién lo cargó y por qué. Filtrar el panel de red por ese dominio y mirar el iniciador de la petición da la respuesta: casi siempre un gestor de etiquetas o un script que carga otro script.

Caso tres: el marco superior es del navegador. La tarea empieza en algo como el análisis del HTML, la ejecución de un temporizador o un manejador de evento sin nombre. Aquí la atribución sale de la pila asíncrona: quién programó ese temporizador, quién registró ese manejador. Es la misma información que usa el depurador para reconstruir el origen de una operación diferida.

Cuando la atribución visual falla, hay un recurso mejor que adivinar: la API de fotogramas de animación largos atribuye cada script automáticamente, con el fichero de origen, el nombre de la función, la posición en el fichero y el tipo de invocador. Es información que el gráfico de llamas tiene pero que hay que reconstruir a mano, y aquí viene ya resuelta.

// Vigilante de tareas largas con atribucion y resumen periodico
(() => {
  const tareas = [];
  const obsTarea = new PerformanceObserver(l => {
    for (const t of l.getEntries()) {
      tareas.push({
        inicioMs: Math.round(t.startTime),
        duracionMs: Math.round(t.duration),
        // La atribucion de longtask es pobre: casi siempre dice "window".
        contenedor: t.attribution?.[0]?.containerType || 'desconocido',
        nombre: t.attribution?.[0]?.containerName || ''
      });
      if (t.duration > 200) console.warn('Tarea de', Math.round(t.duration), 'ms en', Math.round(t.startTime));
    }
  });
  obsTarea.observe({ type: 'longtask', buffered: true });

  window.tareasLargas = () => {
    if (!tareas.length) return 'Ninguna tarea larga registrada todavia.';
    const total = tareas.reduce((s, t) => s + t.duracionMs, 0);
    const bloqueante = tareas.reduce((s, t) => s + Math.max(0, t.duracionMs - 50), 0);
    console.table(tareas.slice().sort((a, b) => b.duracionMs - a.duracionMs).slice(0, 20));
    console.log('Tareas largas:', tareas.length,
      '| Tiempo total en tareas largas:', total, 'ms',
      '| Tiempo bloqueante acumulado:', bloqueante, 'ms');
    return tareas.length;
  };

  console.log('Vigilando tareas largas. Ejecuta tareasLargas() cuando quieras el resumen.');
})();

La métrica que este fragmento calcula al final, la suma de los excesos sobre cincuenta milisegundos, es el tiempo total de bloqueo, y es la magnitud que mejor resume en un solo número cuánto castiga el hilo principal a un usuario que intente interactuar. Es también la métrica que más peso tiene en la puntuación de rendimiento de las auditorías automáticas, precisamente por eso.

ℹ️
Nota

La atribución que ofrece la observación de tareas largas es deliberadamente pobre: por motivos de seguridad, solo dice en qué contenedor ocurrió el trabajo, no qué función. Si necesitas atribución real en producción, el camino es la observación de fotogramas de animación largos, que sí expone el fichero y la función porque solo reporta scripts de orígenes con las cabeceras adecuadas.

Las cuatro estrategias

Encontrada y atribuida la tarea, hay exactamente cuatro cosas que se pueden hacer, en este orden de preferencia.

Eliminarla. La opción que más gente se salta. Antes de acelerar un trabajo, pregunta si tiene que existir. Trabajo que se hace para elementos fuera de la pantalla, cálculos cuyo resultado se descarta, formateos que se recomputan idénticos, datos que se procesan enteros cuando la vista muestra veinte filas: todo eso se elimina, no se optimiza. La eliminación es la única estrategia con ganancia garantizada y sin contrapartida.

Moverla fuera del hilo. Si el trabajo es cómputo puro sobre datos que se pueden transferir —análisis, compresión, cifrado, transformaciones grandes— un worker lo saca del camino por completo. La contrapartida es el coste de mover los datos: si lo que envías es una estructura grande y lo que devuelves también, la serialización puede costar más que el cálculo. Los búferes transferibles evitan la copia y cambian esa ecuación.

Partirla. Si el trabajo tiene que ocurrir en el hilo principal porque toca el DOM, la solución es cortarlo en trozos con cesiones de control entre ellos. El tiempo total no baja, pero el bloqueo desaparece.

// Trocea un trabajo largo cediendo el control cuando toca
async function procesarPorLotes(elementos, trabajo, presupuestoMs = 5) {
  const ceder = () => (
    globalThis.scheduler?.yield?.() ??
    new Promise(r => setTimeout(r, 0))
  );

  let inicioLote = performance.now();
  for (let i = 0; i < elementos.length; i++) {
    trabajo(elementos[i], i);
    if (performance.now() - inicioLote >= presupuestoMs) {
      await ceder();
      inicioLote = performance.now();
    }
  }
}

// Ejemplo ejecutable: mil nodos sin congelar nada
const contenedor = document.createElement('div');
document.body.append(contenedor);
await procesarPorLotes([...Array(1000).keys()], i => {
  const p = document.createElement('p');
  p.textContent = 'Fila numero ' + i;
  contenedor.append(p);
});
console.log('Mil filas insertadas sin bloquear el hilo.');

El presupuesto de cinco milisegundos no es un número mágico: es aproximadamente lo que cabe en un fotograma dejando espacio para que el navegador haga su parte. Bajarlo hace la interfaz más fluida y el trabajo total más lento, porque cada cesión tiene coste.

Diferirla. Si el trabajo no es urgente, programarlo para cuando el navegador no tenga nada mejor que hacer. Es la estrategia correcta para precálculos, precargas y analítica. El riesgo es que el momento inactivo no llegue nunca en una página ocupada, así que conviene un plazo máximo tras el cual se ejecute igualmente.

La tarea larga que más daño hace es la que no está en tu perfil porque tu máquina es demasiado rápida

Hay un sesgo estructural en esta forma de trabajar que conviene tener siempre presente: el umbral de cincuenta milisegundos es absoluto y tu hardware no lo es. Una función que en tu portátil tarda dieciocho milisegundos no aparece marcada, no llama la atención y no entra en ninguna lista de tareas largas. Esa misma función, en el móvil de gama media que usa la mitad de tu público, tarda entre setenta y ciento ochenta milisegundos y es una congelación clara. El perfil que tomaste no contiene el problema, y no porque hayas grabado mal, sino porque en tu máquina el problema no existe. Esto tiene tres consecuencias que cambian la rutina de trabajo. La primera: la ralentización de CPU no es un extra opcional para casos raros, es la condición mínima para que la marca de tarea larga signifique algo. Un perfil sin ralentización responde a la pregunta “qué es lento en un portátil de desarrollo”, que no la ha hecho nadie. La segunda: el umbral relevante para ti es cincuenta dividido entre el factor de ralentización que corresponda a tu público. Si tu usuario objetivo es cuatro veces más lento, cualquier tarea de más de doce milisegundos en tu máquina es sospechosa, y esa es la cifra que hay que mirar cuando por lo que sea no puedes ralentizar. La tercera, y la que más se nota a medio plazo: las tareas largas aparecen por crecimiento de datos, no por cambios de código. Un bucle que procesa la colección del usuario es de tres milisegundos con los veinte elementos que tenía la cuenta de prueba y de novecientos con los seis mil que tiene un cliente real dos años después. Nadie escribió código lento; el código siempre fue lineal y los datos crecieron. Por eso la práctica que de verdad previene este problema no es perfilar más sino probar con volúmenes realistas desde el principio: una cuenta de prueba con el percentil noventa y cinco de datos de tus clientes reales encuentra en cinco minutos lo que un año de perfiles sobre datos de juguete no encuentra nunca.