Encontrar tareas largas antes que tus usuarios

Cualquier trabajo que ocupe el hilo principal más de 50 ms retrasa el siguiente clic. Así se pueden ver.

Una tarea larga (long task) es cualquier cosa que mantenga ocupado el hilo principal del navegador durante más de 50 ms. Mientras se ejecuta, la página no puede responder: un clic espera, una tecla espera, y el usuario lo nota como retraso. La mayoría de las páginas lentas no lo son por la red, sino por unas pocas tareas largas en el momento equivocado.

Verlas

El navegador ya registra las tareas largas. Un PerformanceObserver puede informar de cada una:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`tarea larga: ${Math.round(entry.duration)} ms`, entry);
  }
});
observer.observe({ type: 'longtask', buffered: true });

buffered: true entrega también las tareas que ocurrieron antes de crear el observador, así que se puede registrar tarde sin perderse el trabajo costoso del arranque.

Encontrar al culpable

Una entrada de tarea larga dice que algo fue lento, pero no mucho sobre qué. En los navegadores basados en Chromium, la API más reciente Long Animation Frames es más útil, porque enumera los scripts que se ejecutaron dentro de cada fotograma lento:

new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    for (const script of frame.scripts) {
      console.log(script.invoker, script.sourceURL, Math.round(script.duration));
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });

El campo más revelador suele ser invoker: nombra el listener de eventos, el temporizador o el callback de promesa que inició el trabajo.

Arreglarlas

Pocas veces hace falta que el trabajo en sí sea más rápido. Normalmente basta con dividirlo y dar al navegador la oportunidad de atender al usuario entre medias:

function yieldToMain() {
  if (globalThis.scheduler?.yield) return scheduler.yield();
  return new Promise((resolve) => setTimeout(resolve, 0));
}

async function processAll(items) {
  let last = performance.now();
  for (const item of items) {
    process(item);
    if (performance.now() - last > 40) {
      await yieldToMain();
      last = performance.now();
    }
  }
}

El bucle hace todo lo que cabe en unos 40 ms y luego cede el paso. scheduler.yield() tiene la ventaja de que el trabajo restante se reanuda antes que otras tareas en cola, así que el proceso termina igual de rápido.

← Todas las notas