Presentamos la prueba de origen calendarr.yield

La creación de sitios web que respondan rápidamente a las entradas de los usuarios ha sido uno de los aspectos más desafiantes del rendimiento web. El equipo de Chrome ha trabajado arduamente para ayudar a los desarrolladores web a reunirse. Este año, se anunció que la métrica Interacción a la siguiente pintura (INP) pasaría de ser experimental a pendiente. Ahora está listo para reemplazar el retraso de primera entrada (FID) como Métrica web esencial en marzo de 2024.

En un esfuerzo continuo por ofrecer nuevas APIs que ayuden a los desarrolladores web a que sus sitios web sean 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 incorporación propuesta para la API del programador que ofrece una manera más fácil y eficaz de devolverle el control al subproceso principal que los métodos en los que tradicionalmente se confía.

Cuando se produce una cesión

JavaScript usa el modelo de ejecución hasta finalización para procesar las tareas. Esto significa que, cuando una tarea se ejecuta en el subproceso principal, esta se ejecuta durante el tiempo necesario para completarse. Cuando se completa una tarea, el control se genera de nuevo en el 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 finaliza, como un bucle infinito, por ejemplo, el rendimiento es un aspecto inevitable de la lógica de programación de tareas de JavaScript. Sucederá, solo es cuestión de cuándo, y es mejor hacerlo cuanto antes. 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 baja capacidad de respuesta de la página, ya que retrasan la capacidad del navegador para responder a las entradas del usuario. Cuanto más a menudo se producen las tareas largas (y mientras más se ejecutan), más probable será que los usuarios tengan la impresión de que la página es lenta o incluso que está rota.

Sin embargo, solo porque tu código inicia una tarea en el navegador no significa que debas esperar hasta que se complete esa tarea antes de que se devuelva el control al subproceso principal. Puedes mejorar la capacidad de respuesta a las entradas del usuario en una página si cedes de forma explícita en una tarea, lo que la divide para que se termine 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 terminen las tareas largas.

Representación de cómo dividir una tarea puede facilitar una mejor capacidad de respuesta a las entradas. En la parte superior, una tarea larga impide que se ejecute un controlador de eventos hasta que se termine la tarea. En la parte inferior, la tarea dividida permite que el controlador de eventos se ejecute antes de lo que lo haría de otra manera.
Una visualización de cómo se devuelve el control al subproceso principal. En la parte superior, la cesión se produce solo después de que una tarea se ejecuta hasta completarse, 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 entrega se realiza de forma explícita, lo que divide una tarea larga en varias más pequeñas. Esto permite que las interacciones del usuario se ejecuten antes, lo que mejora la capacidad de respuesta de la entrada y la INP.

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

El problema con las estrategias de rendimiento actuales

Un método común para generar 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 independiente que se pondrá en cola para la ejecución posterior. En lugar de esperar a que el navegador se entregue por sí solo, dices: "Dividamos esta gran cantidad de trabajo en partes más pequeñas".

Sin embargo, generar con setTimeout tiene un efecto secundario potencialmente no deseado: el trabajo que viene después del punto de productividad irá al final de la lista de tareas en cola. Las tareas programadas por las interacciones del usuario seguirán yendo al principio de la cola como deberían, pero el trabajo restante que querías hacer después de ceder explícitamente podría retrasarse aún más por otras tareas de fuentes en competencia que estaban en cola antes que ella.

Para ver esto en acción, prueba esta demostración de Glitch o experimenta con ella en la versión incorporada que aparece a continuación. La demostración consta de algunos botones en los que puedes hacer clic y un cuadro debajo de ellos que registra cuando se ejecutan las tareas. Cuando se encuentre en la página, realice las siguientes acciones:

  1. Haz clic en el botón de la parte superior etiquetado como Run tasks periodically, que programará tareas de bloqueo para que se ejecuten cada tanto. Cuando hagas clic en este botón, el registro de tareas se propagará con varios mensajes que dicen Tarea de bloqueo de ejecución con setInterval.
  2. Luego, haz clic en el botón etiquetado Run loop, yielding with setTimeout on each iteration.

Notarás que el cuadro en la parte inferior de la demostración se leerá de la siguiente manera:

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 demuestra el comportamiento de "fin de la cola de tareas" que se produce cuando se cede con setTimeout. El bucle que se ejecuta procesa cinco elementos y genera setTimeout después de que se procesa cada uno.

Esto ilustra un problema común en la Web: no es inusual que una secuencia de comandos (especialmente una de terceros) registre una función de temporizador que ejecuta trabajo en algún intervalo. El comportamiento de "fin de la lista de tareas en cola" que se genera con el rendimiento con setTimeout significa que el trabajo de otras fuentes de tareas se puede poner en cola antes del trabajo restante que el bucle tiene que hacer después de ceder.

Según tu aplicación, este puede o no ser un resultado conveniente. Sin embargo, 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. La cesión es buena porque las interacciones del usuario tienen la oportunidad de ejecutarse antes, pero también permite que otras interacciones que no son del usuario también tengan tiempo en el subproceso principal. Es un problema real, pero scheduler.yield puede ayudar a resolverlo.

Ingresa scheduler.yield.

scheduler.yield está disponible detrás de una marca como una función experimental de la plataforma web desde la versión 115 de Chrome. Una pregunta que podrías tener es “¿por qué necesito una función especial para generar cuando setTimeout ya lo hace?”.

Vale la pena señalar que la cesión no era un objetivo de diseño de setTimeout, sino un buen efecto secundario 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 procesamiento con setTimeout envía el trabajo restante al atrás de la lista de tareas en cola. De forma predeterminada, scheduler.yield envía el trabajo restante al primer plano de la cola. Esto significa que el trabajo que deseas reanudar inmediatamente después de ceder no pasará a un segundo plano en comparación con las tareas de otras fuentes (con la excepción notable de las interacciones del usuario).

scheduler.yield es una función que cede al subproceso principal y muestra un 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 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 debas reiniciar Chrome después de hacer esto.
  3. Navega a la página de demostración o usa la versión incorporada que se encuentra debajo de esta lista.
  4. Haz clic en el botón de la parte superior etiquetado como Ejecutar tareas periódicamente.
  5. Por último, haz clic en el botón etiquetado como Run loop, yielding with scheduler.yield on each iteration.

El resultado en el cuadro de la parte inferior de la página se verá de la siguiente manera:

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 genera con setTimeout, puedes ver que el bucle, aunque genera después de cada iteración, no envía el trabajo restante al final de la cola, sino al principio. Esto te brinda lo mejor de ambos mundos: puedes ceder para mejorar la capacidad de respuesta de las entradas en tu sitio web, pero también asegurarte de que el trabajo que querías terminar después de ceder 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 quieres experimentar con scheduler.yield de forma local, escribe e ingresa chrome://flags en la barra de direcciones de Chrome y selecciona Habilitar en el menú desplegable de la sección Funciones experimentales de la plataforma web. Esto hará que scheduler.yield (y cualquier otra función experimental) esté 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 en la prueba de origen de scheduler.yield. Esto te permite experimentar de forma segura con las funciones propuestas durante un período determinado y le brinda al equipo de Chrome estadísticas valiosas sobre cómo se usan 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 (sin dejar de admitir navegadores que no la implementen) depende de tus objetivos. Puedes usar el polyfill oficial. El polyfill es útil si la siguiente situación se aplica a tu caso:

  1. Ya estás usando scheduler.postTask en tu aplicación para programar tareas.
  2. Quieres poder establecer prioridades de tareas y rendimientos.
  3. Supongamos que quieres poder cancelar o repriorizar tareas a través de la clase TaskController que ofrece la API de scheduler.postTask.

Si esta no es tu situación, es posible que el polyfill no sea adecuado para ti. En ese caso, puedes implementar 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 imaginar, los navegadores que no admiten scheduler.yield no mostrarán el comportamiento de "fila de prioridad". Si eso significa que prefieres no generar rendimientos, puedes probar otro enfoque que use scheduler.yield si está disponible, pero que no genere rendimientos 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 un complemento emocionante para la API del programador, que con suerte facilitará a los desarrolladores mejorar la capacidad de respuesta que las estrategias de rendimiento actuales. Si crees que scheduler.yield es una API útil, participa en nuestra investigación para ayudar a mejorarla y envíanos tus comentarios sobre cómo podría mejorarse aún más.

Imagen hero de Unsplash, por Jonathan Allison.