Fecha de publicación: 6 de marzo de 2025
Una página se siente lenta y no responde cuando las tareas largas mantienen ocupado el subproceso principal, lo que le impide realizar otras tareas importantes, como responder a las entradas del usuario. Como resultado, incluso los controles de formulario integrados pueden parecer dañados para los usuarios, como si la página estuviera inmovilizada, sin mencionar los componentes personalizados más complejos.
scheduler.yield()
es una forma de ceder al subproceso principal, lo que permite que el navegador ejecute cualquier trabajo pendiente de alta prioridad y, luego, continúe la ejecución donde se detuvo. Esto mantiene una página más responsiva y, a su vez, ayuda a mejorar la interacción hasta la siguiente pintura (INP).
scheduler.yield
ofrece una API ergonómica que hace exactamente lo que dice: la ejecución de la función a la que se llama se detiene en la expresión await scheduler.yield()
y cede al subproceso principal, lo que divide la tarea. La ejecución del resto de la función, llamada Continuation, 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 de que se genera el 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 en lugar de iniciar 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 las tareas nuevas que ya están en cola, lo que podría generar demoras prolongadas entre ceder el subproceso principal y completar su trabajo.
Continuaciones con prioridad después de ceder
scheduler.yield
forma parte de la API de programación de tareas priorizadas. Como desarrolladores web, por lo general, no hablamos 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 requestIdleCallback
que se ejecuta después de cualquier devolución de llamada setTimeout
en cola, o un objeto de escucha de eventos de entrada activado que, por lo general, se ejecuta 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 la determinación de 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()
tiene una prioridad más alta que iniciar otras tareas. El concepto principal es que la continuación de una tarea debe ejecutarse primero, antes de pasar a otras tareas. Si la tarea es un código con buen comportamiento que se entrega periódicamente para que el navegador pueda hacer otras tareas importantes (como responder a la entrada del usuario), no se debe castigar por entregarse y priorizarse después de otras tareas similares.
Este es un ejemplo: dos funciones en fila 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, se podría llamar a ellas en lugares completamente diferentes, como una secuencia de comandos propia y una de terceros que configuran el trabajo de forma independiente para que se ejecute, o bien podrían ser dos tareas de componentes separados que se activan en lo profundo del programador de tu framework.
Así es como se vería ese trabajo en DevTools:
myJob
se marca como una tarea larga, lo que impide que el navegador haga otra cosa mientras se ejecuta. Si suponemos que proviene de una secuencia de comandos propia, podemos dividirla de la siguiente manera:
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:
Dividimos la tarea con setTimeout
para que el navegador pueda ser responsivo durante el medio de myJob
, pero ahora la segunda parte de myJob
solo se ejecuta después de que finaliza someoneElsesJob
.
En algunos casos, eso puede estar bien, pero, por lo general, no es lo más adecuado. myJob
cedía al subproceso principal para asegurarse de que la página pudiera seguir siendo responsiva a la entrada del usuario, no para abandonar el subproceso principal por completo. En los casos en que someoneElsesJob
es especialmente lento o se programaron muchos otros trabajos además de someoneElsesJob
, podría transcurrir 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 iniciar cualquier otra tarea similar. Si se cambia myJob
para usarlo, sucede 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:
El navegador aún tiene la oportunidad de ser responsivo, pero ahora la continuación de la tarea myJob
tiene prioridad sobre el inicio de la tarea nueva someoneElsesJob
, por lo que myJob
se completa antes de que comience someoneElsesJob
. Esto está mucho más cerca de la expectativa de ceder al subproceso principal para mantener la capacidad de respuesta, en lugar de abandonarlo por completo.
Herencia de prioridad
Como parte de la API de programación de tareas priorizadas 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, como usar 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 superior a la de otras tareas de 'background'
(obteniendo la continuación priorizada esperada antes de cualquier trabajo pendiente de 'background'
), pero sigue siendo una prioridad más baja que otras tareas predeterminadas o de alta prioridad; sigue siendo un trabajo de 'background'
.
Esto significa que, si programas un trabajo de baja prioridad con un scheduler.postTask()
'background'
(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 otras tareas y el subproceso principal esté inactivo para ejecutarse, que es exactamente lo que deseas obtener de la entrega en un trabajo de baja prioridad.
Cómo usar la API
Por ahora, scheduler.yield()
solo está disponible en navegadores basados en Chromium, por lo que, para usarlo, deberás detectar funciones y recurrir a una forma secundaria de generar resultados para otros navegadores.
scheduler-polyfill
es un pequeño polyfill para scheduler.postTask
y scheduler.yield
que, de forma interna, usa 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 generar con setTimeout()
y aceptar la pérdida de una continuación priorizada, o incluso no generar 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 verificación de tipos y compatibilidad con IDE si detectas funciones scheduler.yield()
y agregas un resguardo 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 la documentación de scheduler.yield()
y Programación de tareas priorizadas 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 cómo optimizar las tareas largas.