Usa scheduler.yield() para dividir tareas largas

Brendan Kenny
Brendan Kenny

Publicado: 6 de marzo de 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

Una página se siente lenta y no responde cuando las tareas largas mantienen ocupado el subproceso principal, lo que impide que realice otro trabajo importante, como responder a la entrada del usuario. Como resultado, incluso los controles de formulario integrados pueden parecer rotos para los usuarios, como si la página estuviera congelada, sin mencionar los componentes personalizados más complejos.

scheduler.yield() es una forma de ceder el control al subproceso principal, lo que permite que el navegador ejecute cualquier trabajo pendiente de alta prioridad y, luego, continúe la ejecución donde la dejó. Esto mantiene la página más responsiva y, a su vez, ayuda a mejorar la interacción a la siguiente pintura (INP).

scheduler.yield ofrece una API ergonómica que hace exactamente lo que dice: la ejecución de la función en la que se llama se detiene en la expresión await scheduler.yield() y cede el control al subproceso principal, lo que interrumpe la tarea. La ejecución del resto de la función, denominada continuación de la función, se programará para que se ejecute en una nueva tarea de bucle de eventos.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

El beneficio específico de scheduler.yield es que la continuación después del rendimiento se programa para ejecutarse antes de ejecutar cualquier otra tarea similar que la página haya puesto en cola. Prioriza la continuación de una tarea por sobre el inicio de tareas nuevas.

Las funciones como setTimeout o scheduler.postTask también se pueden usar para dividir tareas, pero esas continuaciones suelen ejecutarse después de cualquier tarea nueva que ya esté en la cola, lo que podría generar demoras prolongadas entre la cesión al subproceso principal y la finalización de su trabajo.

Continuaciones priorizadas después de la generación

scheduler.yield forma parte de la API de Prioritized Task Scheduling. Como desarrolladores web, no solemos hablar del orden en que el bucle de eventos ejecuta tareas en términos de prioridades explícitas, pero las prioridades relativas siempre están ahí, como una devolución de llamada de requestIdleCallback que se ejecuta después de cualquier devolución de llamada de setTimeout en cola, o un objeto de escucha de eventos de entrada activado que suele ejecutarse antes de una tarea en cola con setTimeout(callback, 0).

La programación de tareas priorizadas solo hace que esto sea más explícito, lo que facilita determinar qué tarea se ejecutará antes que otra, y permite ajustar las prioridades para cambiar ese orden de ejecución, si es necesario.

Como se mencionó, la ejecución continua de una función después de ceder con scheduler.yield() obtiene una prioridad más alta que el inicio de otras tareas. El concepto guía es que la continuación de una tarea debe ejecutarse primero, antes de pasar a otras tareas. Si la tarea es un código bien estructurado que cede periódicamente para que el navegador pueda realizar otras tareas importantes (como responder a la entrada del usuario), no se debe castigar por ceder priorizándola después de otras tareas similares.

Este es un ejemplo: dos funciones en cola para ejecutarse en diferentes tareas con setTimeout.

setTimeout(myJob);
setTimeout(someoneElsesJob);

En este caso, las dos llamadas a setTimeout están una al lado de la otra, pero en una página real, podrían llamarse en lugares completamente diferentes, como un script de origen y un script de terceros que configuran de forma independiente el trabajo para que se ejecute, o podrían ser dos tareas de componentes separados que se activan en lo profundo del programador de tu framework.

Así se vería ese trabajo en Herramientas para desarrolladores:

Dos tareas que se muestran en el panel de rendimiento de Herramientas para desarrolladores de Chrome. Ambas se indican como tareas largas, con la función "myJob" que ocupa toda la ejecución de la primera tarea y "someoneElsesJob" que ocupa toda la segunda tarea.

myJob se marca como una tarea larga, lo que impide que el navegador realice cualquier otra acción mientras se ejecuta. Si suponemos que proviene de una secuencia de comandos propia, podemos desglosarla:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

Como myJobPart2 se programó para ejecutarse con setTimeout dentro de myJob, pero esa programación se ejecuta después de que ya se programó someoneElsesJob, la ejecución se verá de la siguiente manera:

Se muestran tres tareas en el panel de rendimiento de Herramientas para desarrolladores de Chrome. La primera ejecuta la función "myJobPart1", la segunda ejecuta una tarea larga "someoneElsesJob" y, por último, la tercera ejecuta "myJobPart2".

Dividimos la tarea con setTimeout para que el navegador pueda responder durante la mitad de myJob, pero ahora la segunda parte de myJob solo se ejecuta después de que finaliza someoneElsesJob.

En algunos casos, esto puede estar bien, pero, por lo general, no es lo óptimo. myJob cedía el control al subproceso principal para asegurarse de que la página pudiera seguir respondiendo a la entrada del usuario, no para renunciar por completo al subproceso principal. En los casos en que someoneElsesJob es especialmente lento o se programaron muchos otros trabajos además de someoneElsesJob, podría pasar mucho tiempo antes de que se ejecute la segunda mitad de myJob. Probablemente, esa no era la intención del desarrollador cuando agregó ese setTimeout a myJob.

Ingresa scheduler.yield(), que coloca la continuación de cualquier función que la invoque en una cola de prioridad ligeramente más alta que la de cualquier otra tarea similar. Si se cambia myJob para usarlo, haz lo siguiente:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

Ahora, la ejecución se ve de la siguiente manera:

Dos tareas que se muestran en el panel de rendimiento de Herramientas para desarrolladores de Chrome. Ambas se indican como tareas largas, con la función "myJob" que ocupa toda la ejecución de la primera tarea y "someoneElsesJob" que ocupa toda la segunda tarea.

El navegador aún tiene la oportunidad de responder, pero ahora se prioriza la continuación de la tarea myJob por sobre el inicio de la nueva tarea someoneElsesJob, por lo que myJob se completa antes de que comience someoneElsesJob. Esto se acerca mucho más a la expectativa de ceder el subproceso principal para mantener la capacidad de respuesta, no de renunciar por completo al subproceso principal.

Herencia de prioridad

Como parte de la API de Prioritized Task Scheduling más grande, scheduler.yield() se compone bien con las prioridades explícitas disponibles en scheduler.postTask(). Sin una prioridad establecida de forma explícita, un scheduler.yield() dentro de una devolución de llamada scheduler.postTask() actuará básicamente de la misma manera que en el ejemplo anterior.

Sin embargo, si se establece una prioridad, por ejemplo, con una prioridad 'background' baja, sucede lo siguiente:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

La continuación se programará con una prioridad más alta que otras tareas de 'background' (se obtendrá la continuación priorizada esperada antes que cualquier trabajo de 'background' pendiente), pero con una prioridad más baja que otras tareas predeterminadas o de alta prioridad. Seguirá siendo un trabajo de 'background'.

Esto significa que, si programas trabajo de baja prioridad con un 'background' scheduler.postTask() (o con requestIdleCallback), la continuación después de un scheduler.yield() dentro también esperará hasta que se completen la mayoría de las demás tareas y el subproceso principal esté inactivo para ejecutarse, que es exactamente lo que deseas de la cesión en un trabajo de baja prioridad.

Cómo usar la API

Por el momento, scheduler.yield() solo está disponible en los navegadores basados en Chromium, por lo que, para usarlo, deberás detectar la función y recurrir a una forma secundaria de ceder para otros navegadores.

scheduler-polyfill es un pequeño polyfill para scheduler.postTask y scheduler.yield que usa internamente una combinación de métodos para emular gran parte de la potencia de las APIs de programación en otros navegadores (aunque no se admite la herencia de prioridad de scheduler.yield()).

Para quienes buscan evitar un polyfill, un método es ceder el control con setTimeout() y aceptar la pérdida de una continuación priorizada, o incluso no ceder el control en navegadores no compatibles si eso no es aceptable. Consulta la documentación de scheduler.yield() en Optimiza tareas largas para obtener más información.

Los tipos wicg-task-scheduling también se pueden usar para obtener la verificación de tipos y la compatibilidad con el IDE si detectas la función scheduler.yield() y agregas una alternativa por tu cuenta.

Más información

Para obtener más información sobre la API y cómo interactúa con las prioridades de tareas y scheduler.postTask(), consulta los documentos scheduler.yield() y Prioritized Task Scheduling en MDN.

Para obtener más información sobre las tareas largas, cómo afectan la experiencia del usuario y qué hacer al respecto, consulta el artículo sobre la optimización de tareas largas.