setTimeout: optimización de tareas largas en JS
El método que utiliza la función setTimeout() sirve para optimizar tareas largas en JavaScript (medibles mediante la métrica JS Long Tasks (JSLT)), que bloquean interacciones de usuario y deterioran métricas como Total Blocking Time (TBT) o Interaction to Next Paint (INP), que es una parte crucial de la colección Core Web Vitals.
¿El problema? Tareas largas en JavaScript
JavaScript en el navegador opera con un único hilo. En la práctica, esto significa que todas las acciones, eventos e interacciones se alinean en colas gestionadas por un mecanismo llamado event loop.
Desde la perspectiva de la optimización de velocidad web, surge un problema cuando una tarea dura demasiado y bloquea otras tareas. Esto, en consecuencia, bloquea todo el navegador y su hilo principal, conocido como main thread. Estas tareas largas se miden con la métrica JS Long Tasks (JSLT).
Las tareas largas duran más de 50 ms y al medirlas en DevTools del navegador se identifican por el resaltado rojo.
setTimeout() y optimización de INP
Para optimizar un alto valor de la métrica INP, es necesario segmentar el código según su importancia, posponer las partes menos críticas y así dejar espacio al navegador para procesar otras interacciones del usuario.
Antes de la optimización, el usuario debe esperar a que una tarea larga ("Task") se complete para activar una acción ("Event") después de recibir una entrada ("Input received"). Tras la optimización, la tarea larga se divide en partes más pequeñas permitiendo que la acción se ejecute antes.
En JavaScript, se pueden manejar tareas largas mediante el uso de operaciones asíncronas. Entre ellas, destaca el API de temporizadores, encabezado por la función window.setTimeout(). Esta permite posponer tareas seleccionadas. La función setTimeout() tiene además una característica única. Si se configura con un retraso de cero, es probable que el navegador posponga la tarea al siguiente ciclo de renderizado:
setTimeout(() => {
tarea_pospuesta_al_siguiente_renderizado();
}, 0);
Gracias a esta característica, es posible dividir el código rápidamente y posponer tareas menos importantes un ciclo de renderizado, creando así el espacio necesario para que el navegador tome control y muestre el resultado del código importante en la pantalla:
function saveSettings() {
// Realiza trabajo crítico visible para el usuario:
validateForm();
showSpinner();
updateUI();
// Pospone trabajo no visible para el usuario a una tarea separada:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
En el ciclo de renderizado actual, realizamos una actualización en la UI. La escritura del evento en la base de datos y el análisis se pospone para ejecutarse de manera asíncrona, dividiendo así la tarea en dos.
Buen servidor, pero mal amo
Nada es gratis. Este método de optimización parece sencillo, pero en la sencillez a menudo yace el diablo. Hay situaciones en las que posponer código con setTimeout puede causar complicaciones.
Cuidado con la analítica
Posponer el seguimiento de un clic en un enlace puede causar un desastre analítico. Si el clic provoca la carga de una nueva página (no se refiere al enrutamiento SPA), la ejecución de JavaScript se detiene y el código pospuesto con setTimeout(fn, 0) no se ejecutará. Pospone solo aquellas mediciones donde sepas que el usuario permanece en la misma página.
Cantidad de tareas pospuestas
Las tareas se alinean en la cola para ser procesadas más tarde con setTimeout(fn, 0). Si pospones muchas tareas y trabajo, crearás nuevamente una tarea larga en el futuro. Además, debes recordar que el navegador tiene otras cosas que hacer además de procesar tus temporizadores. Posponer de manera consciente y cuidadosa. También puedes experimentar un poco con el segundo parámetro del API, es decir, el tiempo, y así influir virtualmente en la prioridad de las tareas.
Frameworks de JavaScript
Todos los frameworks modernos de JavaScript tienen su propio mecanismo para manejar tareas e interacciones. Llamar imprudentemente a setTimeout(fn, 0) podría causar un conflicto de estados. Siempre infórmate sobre el procedimiento recomendado en tu framework para posponer código un ciclo de renderizado. Al optimizar React, ten cuidado, useEffect no siempre es asíncrono.
Todo esto es un "hack"
Se podría decir que desde la perspectiva de la optimización de tareas largas, usar la función setTimeout() es un hack. Al fin y al cabo, es un API destinado a programar y ejecutar una tarea en algún momento futuro.
No hay conexión directa con el rendimiento del navegador aquí, y que el código se ejecute en el siguiente ciclo de renderizado es más bien un efecto secundario. La especificación está reaccionando a esta necesidad, y están surgiendo nuevos APIs como scheduler.postTask() y scheduler.yield(). El soporte global en los navegadores aún no está disponible. Con el uso de "graceful degradation", es posible experimentar con el API.
Diferencia entre setTimeout y scheduler.yield(). Una tarea dividida con yield tendrá mayor prioridad que otras tareas en la cola.
Otras alternativas para posponer tareas
En JavaScript, por supuesto, existen más maneras de posponer la ejecución de algún código. Pero son similares a setTimeout, ya que están destinadas principalmente a otros propósitos.
- El API
window.requestAnimationFrame()se menciona a menudo como una alternativa. Sin embargo, está destinada a sincronizar estilos calculados con elementos DOM. Es decir, es ideal para elementos animados que se animan con JavaScript. Si usas este API para "dividir tareas", probablemente solo empeorarás la velocidad de respuesta. - El API
window.requestIdleCallback()sirve para optimizar la velocidad y posponer tareas hasta que el navegador está ocioso. Sin embargo, no tienes control sobre el tiempo de procesamiento, por lo que para algunas acciones, como la analítica, puede ser completamente inadecuado. - El API
MessageChannel(), también se puede usar para posponer código al siguiente ciclo de renderizado. Internamente, lo utiliza React. Pero, nuevamente, es solo otro hack.
Conclusión
Aunque sabemos que setTimeout es un "hack para optimizar INP", tiene una ventaja fundamental. Su uso es muy sencillo y rápido.
En términos de soporte, sigue siendo la única solución infalible, ya que el API de temporizadores ha existido en JavaScript desde siempre. Sin embargo, si puedes, opta por el más moderno API scheduler.yield() u otras soluciones sostenibles a largo plazo.
¿Hacia dónde ir ahora?
- Aprende sobre otras métodos para optimizar INP.
- ¿Te gusta un framework específico? Echa un vistazo a la optimización de React o Vue.
- Permítenos ayudarte con el análisis de la métrica INP.