Presentamos la prueba de origen calendarr.yield

La compilación de sitios web que respondan rápidamente a las entradas del usuario ha sido uno de los aspectos más desafiantes del rendimiento web, y el equipo de Chrome ha trabajado arduamente para ayudar a los desarrolladores web a cumplir con este objetivo. Este año, se anunció que la métrica Interaction to Next Paint (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 rápidos 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 a la API del programador que permite una forma más fácil y mejor de devolver el control al subproceso principal que los métodos que se usaban tradicionalmente.

Cuando se produce una cesión

JavaScript usa el modelo de ejecución hasta la finalización para controlar 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 devuelve al subproceso principal, lo que le permite procesar la siguiente tarea de la cola.

Aparte de los casos extremos en los que una tarea nunca termina (como un bucle infinito, por ejemplo), la entrega 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 produzcan tareas largas y cuanto más tiempo se ejecuten, es más probable que los usuarios tengan la impresión de que la página es lenta o incluso que está completamente dañada.

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 de entrada. 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 en el kit de herramientas de un desarrollador que puede ser muy útil para 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, ceder con setTimeout tiene un efecto secundario potencialmente no deseado: el trabajo que se produce después del punto de cesión irá al final de la cola de tareas. 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 que compiten 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 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. Cuando llegues a la página, realiza 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 Se ejecutó la tarea de bloqueo 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 dirá algo como lo siguiente:

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, en particular una de terceros, registre una función de temporizador que ejecute tareas en algún intervalo. El comportamiento de "fin de la cola de tareas" que se obtiene con la entrega con setTimeout significa que el trabajo de otras fuentes de tareas puede ponerse en cola antes del trabajo restante que el bucle debe hacer después de la entrega.

Según tu aplicación, este puede ser o no un resultado deseable, pero, en muchos casos, este comportamiento es el motivo por el que los desarrolladores pueden sentirse reacios a renunciar al 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 ayudarte 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, lo más importante que debes recordar es que ceder con setTimeout envía el trabajo restante a la parte posterior de la cola de tareas. De forma predeterminada, scheduler.yield envía el trabajo restante al principio 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 hacerlo.
  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 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. 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 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 lo implementen) depende de tus objetivos. Puedes usar el polyfill oficial. El polyfill es útil si se cumple alguna de las siguientes condiciones:

  1. Ya usas scheduler.postTask en tu aplicación para programar tareas.
  2. Quieres poder establecer prioridades de tareas y rendimientos.
  3. 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.