Presentamos la prueba de origen calendarr.yield

Uno de los aspectos más desafiantes del rendimiento web fue crear sitios web que respondan rápidamente a las entradas de los usuarios, y el equipo de Chrome trabajó mucho para ayudar a los desarrolladores web a reunirse. Justo este año, se anunció que la métrica de interacción a la siguiente pintura (INP) pasaría del estado experimental al estado pendiente. Ahora está lista para reemplazar el Retraso de primera entrada (FID) como una Métrica web esencial en marzo de 2024.

En un esfuerzo continuo por ofrecer nuevas APIs que ayuden a los desarrolladores web a crear sitios web lo más ágiles posible, el equipo de Chrome está ejecutando una prueba de origen para scheduler.yield a partir de la versión 115 de Chrome. scheduler.yield es una nueva adición propuesta a la API de Scheduler que permite una forma más sencilla y eficaz de ceder control al subproceso principal que los métodos en los que se basaba tradicionalmente.

Al ceder

JavaScript usa el modelo de ejecución completa para gestionar tareas. Esto significa que, cuando una tarea se ejecuta en el subproceso principal, se ejecuta el tiempo necesario para completarse. Cuando se completa una tarea, el control se envía al subproceso principal, lo que permite que este procese la siguiente tarea de la cola.

Aparte de los casos extremos en los que una tarea nunca se completa (como los bucles infinitos, por ejemplo), la producción es un aspecto inevitable de la lógica de programación de tareas de JavaScript. Ocurrirá, es solo una cuestión de cuándo, y antes es mejor que después. Cuando las tareas tardan demasiado en ejecutarse (más de 50 milisegundos, para ser exactos), se consideran tareas largas.

Las tareas largas son una fuente de respuesta de la página deficiente porque retrasan la capacidad del navegador de responder a las entradas del usuario. Cuanto más a menudo se realizan las tareas largas (y más tiempo se ejecutan), más probable será que los usuarios tengan la impresión de que la página es lenta o, incluso, sienten que está completamente dañada.

Sin embargo, el hecho de que tu código inicie una tarea en el navegador no significa que debas esperar hasta que esa tarea finalice para que el control se entregue al subproceso principal. Puedes mejorar la capacidad de respuesta a la entrada del usuario en una página produciendo un trabajo de forma explícita en una tarea, que divide la tarea para terminarla en la próxima oportunidad disponible. Esto permite que otras tareas obtengan tiempo en el subproceso principal antes que si tuvieran que esperar a que se completen las tareas largas.

Representación de cómo dividir una tarea puede facilitar una mejor capacidad de respuesta de las entradas. En la parte superior, una tarea larga bloquea la ejecución de un controlador de eventos hasta que la tarea finalice. En la parte inferior, la tarea fragmentada permite que el controlador de eventos se ejecute antes de lo que lo haría de otro modo.
Visualización de cómo volver a controlar el subproceso principal En la parte superior, la generación se produce solo después de que se completa una tarea, lo que significa que las tareas pueden tardar más en completarse antes de devolver el control al subproceso principal. En la parte inferior, la generación se hace explícitamente, con lo que se divide una tarea larga en varias más pequeñas. Esto permite que las interacciones del usuario se ejecuten más rápido, lo que mejora la capacidad de respuesta de las entradas y el INP.

Cuando respondes explícitamente, le dices al navegador: "Entiendo que el trabajo que estoy por hacer podría llevar un tiempo, y no quiero que tengas que hacer todo ese trabajo antes de responder a las entradas del usuario o a otras tareas que también podrían ser importantes". Es una herramienta valiosa de la caja de herramientas de un desarrollador que puede contribuir en gran medida a mejorar la experiencia del usuario.

El problema de las estrategias actuales de producción

Un método común de producción usa setTimeout con un valor de tiempo de espera de 0. Esto funciona porque la devolución de llamada que se pasa a setTimeout moverá el trabajo restante a una tarea separada que se pondrá en cola para su posterior ejecución. En lugar de esperar a que el navegador produzca por sí solo, estás diciendo: "dividamos esta gran porción de trabajo en partes más pequeñas".

Sin embargo, generar con setTimeout lleva un efecto secundario potencialmente no deseado: el trabajo que viene después del punto de productividad se moverá al final de la lista de tareas en cola. Las tareas programadas por las interacciones del usuario seguirán en el primer lugar de la cola como corresponde, pero el trabajo restante que querías hacer después de generarlo explícitamente podría retrasarse aún más por otras tareas de fuentes rivales que se colocaron en cola antes que él.

Para ver cómo funciona, prueba esta demostración de Glitch o experimenta con ella en la versión incorporada a continuación. La demostración consta de algunos botones en los que puedes hacer clic y un cuadro debajo de ellos que registra cuándo se ejecutan las tareas. Una vez que se encuentra en la página, realice las siguientes acciones:

  1. Haz clic en el botón superior etiquetado como Ejecutar tareas periódicamente, que programará la ejecución de tareas de bloqueo de manera periódica. Cuando hagas clic en este botón, el registro de tareas se propagará con varios mensajes que dicen Se ejecutó la tarea de bloqueo con setInterval.
  2. Luego, haz clic en el botón con la etiqueta Ejecutar bucle, que genera con setTimeout en cada iteración.

Notarás que el cuadro en la parte inferior de la demostración dirá algo como esto:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Este resultado muestra el comportamiento del “fin de la lista de tareas en cola” que se produce cuando se produce con setTimeout. El bucle que ejecuta procesa cinco elementos y genera setTimeout después de que cada uno se procesa.

Esto ilustra un problema común en la Web: no es inusual que una secuencia de comandos, en particular una de terceros, registre una función de temporizador que se ejecuta en algún intervalo. El comportamiento del "fin de la lista de tareas en cola" que viene con la producción con setTimeout significa que el trabajo de otras fuentes de tareas puede quedar en cola antes que el trabajo restante que el bucle debe hacer después de la producción.

Según tu aplicación, este puede ser un resultado deseable o no, pero, en muchos casos, este comportamiento es el motivo por el que los desarrolladores pueden sentirse reacios a ceder el control del subproceso principal con tanta facilidad. El rendimiento es bueno porque las interacciones del usuario tienen la oportunidad de ejecutarse antes, pero también permite que otras interacciones que no son del usuario obtengan tiempo en el subproceso principal. Es un problema real, pero scheduler.yield puede ayudar a resolverlo.

Ingresar scheduler.yield

scheduler.yield está disponible detrás de una marca como una función de plataforma web experimental desde la versión 115 de Chrome. Quizás te preguntes: "¿por qué necesito una función especial para producir cuando setTimeout ya lo hace?".

Vale la pena señalar que el rendimiento no era un objetivo de diseño de setTimeout, sino un buen efecto secundario a la hora de programar una devolución de llamada para que se ejecute más adelante, incluso con un valor de tiempo de espera de 0 especificado. Sin embargo, es más importante recordar que el rendimiento con setTimeout envía el trabajo restante al back de la lista de tareas en cola. De forma predeterminada, scheduler.yield envía el trabajo restante al frente de la cola. Esto significa que el trabajo que quieras reanudar inmediatamente después de ceder el trabajo no pasará a segundo plano para las tareas de otras fuentes (con la notable excepción de las interacciones de los usuarios).

scheduler.yield es una función que entrega el subproceso principal y muestra una Promise cuando se la llama. Esto significa que puedes await en una función async:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

Para ver a scheduler.yield en acción, haz lo siguiente:

  1. Navega a chrome://flags.
  2. Habilita el experimento Funciones experimentales de la plataforma web. Es posible que tengas que reiniciar Chrome después de hacerlo.
  3. Navega a la página de demostración o usa la versión incorporada de la que se encuentre debajo de esta lista.
  4. Haz clic en el botón superior etiquetado como Ejecutar tareas periódicamente.
  5. Por último, haz clic en el botón Ejecutar bucle, que genera con scheduler.yield en cada iteración.

El resultado en el cuadro que se encuentra en la parte inferior de la página será similar al siguiente:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

A diferencia de la demostración que se genera con setTimeout, puedes ver que el bucle, aunque se produce después de cada iteración, no envía el trabajo restante al final de la cola, sino al principio de esta. Esto te ofrece lo mejor de ambos mundos: puedes mejorar la capacidad de respuesta de las entradas en tu sitio web y asegurarte de que el trabajo que querías finalizar después de generarlo no se retrase.

¡Pruébalo!

Si scheduler.yield te parece interesante y quieres probarlo, puedes hacerlo de dos maneras a partir de la versión 115 de Chrome:

  1. Si deseas experimentar con scheduler.yield a nivel local, escribe chrome://flags en la barra de direcciones de Chrome y, luego, selecciona Habilitar en el menú desplegable de la sección Funciones experimentales de la plataforma web. De esta manera, scheduler.yield (y cualquier otra función experimental) estará disponible solo en tu instancia de Chrome.
  2. Si quieres habilitar scheduler.yield para usuarios reales de Chromium en un origen de acceso público, deberás registrarte para la prueba de origen de scheduler.yield. Esto te permite experimentar de forma segura con las funciones propuestas durante un período determinado y brinda al equipo de Chrome estadísticas valiosas sobre cómo se utilizan esas funciones en el campo. Para obtener más información sobre cómo funcionan las pruebas de origen, lee esta guía.

La forma en que uses scheduler.yield (si bien admites navegadores que no lo implementan) depende de tus objetivos. Puedes usar el polyfill oficial. El polyfill es útil si las siguientes situaciones se aplican a tu situación:

  1. Ya estás usando scheduler.postTask en tu aplicación para programar tareas.
  2. Deseas poder establecer tareas y prioridades de rendimiento.
  3. Deseas poder cancelar o volver a priorizar tareas a través de la clase TaskController que ofrece la API de scheduler.postTask.

Si esto no describe tu situación, es posible que el polyfill no sea adecuado para ti. En ese caso, puedes revertir tu propio resguardo de varias maneras. El primer enfoque usa scheduler.yield si está disponible, pero recurre a setTimeout si no lo está:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Esto puede funcionar, pero como puedes suponer, los navegadores que no son compatibles con scheduler.yield producirán un error sin el comportamiento de "primera fila". Si eso significa que prefieres no producir el rendimiento en absoluto, puedes probar otro enfoque que use scheduler.yield si está disponible, pero que no produzca ningún resultado si no lo está:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield es una incorporación emocionante a la API de Scheduler que esperamos que ayude a los desarrolladores a mejorar la capacidad de respuesta con mayor facilidad que las estrategias de rendimiento actuales. Si scheduler.yield te parece una API útil, participa en nuestra investigación para ayudarnos a mejorar y envíanos comentarios sobre cómo podemos seguir mejorando.

Hero image de Unsplash, de Jonathan Allison.