Presentamos la prueba de origen calendarr.yield

Crear sitios web que respondan rápidamente a la entrada 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 de Interaction to Next Paint (INP) pasaría de experimental a pendiente. Ahora está lista para reemplazar el First Input Delay (FID) como Core Web Vital en marzo de 2024.

En un esfuerzo continuo por ofrecer nuevas APIs que ayuden a los desarrolladores web a hacer que sus sitios web sean lo más rápidos posible, el equipo de Chrome actualmente 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 ceder el control al subproceso principal que los métodos en los que se ha confiado tradicionalmente.

Cuando se cede

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

Además de los casos extremos en los que una tarea nunca finaliza (por ejemplo, un bucle infinito), la cesión es un aspecto inevitable de la lógica de programación de tareas de JavaScript. Sucederá, solo es cuestión de cuándo, y cuanto antes, mejor. 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 las 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 a que finalice esa tarea antes de que el control se devuelva al subproceso principal. Puedes mejorar la capacidad de respuesta a la entrada del usuario en una página si cedes explícitamente en una tarea, lo que divide la tarea para que se complete en la próxima oportunidad disponible. Esto permite que otras tareas tengan tiempo en el subproceso principal antes que si tuvieran que esperar a que finalicen 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 finalice 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 otro modo.
Una visualización de la devolución del control al subproceso principal. En la parte superior, la cesión solo se produce 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 el nivel más bajo, la cesión 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 el INP.

Cuando cedes explícitamente, le dices al navegador: "Entiendo que el trabajo que estoy a punto de hacer podría llevar 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 la caja 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 ceder 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 ejecución posterior. En lugar de esperar a que el navegador ceda por sí solo, le dices "divide este gran fragmento de trabajo en partes más pequeñas".

Sin embargo, ceder con setTimeout conlleva un efecto secundario potencialmente no deseado: el trabajo que se realiza después del punto de cesión pasará al final de la cola de tareas. Las tareas programadas por las interacciones del usuario seguirán pasando al frente de la cola como deberían, pero el trabajo restante que querías hacer después de ceder explícitamente podría demorarse aún más por otras tareas de fuentes competitivas que se pusieron en cola antes.

Para ver cómo funciona, prueba esta demostración de Codepen o experimenta con ella en la siguiente versión incorporada. 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 superior etiquetado como Ejecutar tareas de forma periódica, que programará tareas de bloqueo para que se ejecuten con frecuencia. Cuando hagas clic en este botón, el registro de tareas se completará con varios mensajes que dicen Se ejecutó la tarea de bloqueo con setInterval.
  2. A continuación, haz clic en el botón etiquetado como Run loop, yielding with setTimeout on each iteration.

Verá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 lista de tareas en cola" que se produce cuando se cede con setTimeout. El bucle que se ejecuta procesa cinco elementos y genera un resultado con 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 trabajo en algún intervalo. El comportamiento de "fin de la cola de tareas" que se produce al ceder con setTimeout significa que el trabajo de otras fuentes de tareas puede ponerse en cola antes del trabajo restante que el bucle debe realizar después de ceder.

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 dudar en 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 otro trabajo que no sea de interacción del usuario también tenga 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 hacerte es: "¿Por qué necesito una función especial para ceder cuando setTimeout ya lo hace?".

Cabe destacar que la cesión no fue un objetivo de diseño de setTimeout, sino un efecto secundario agradable en la programación de una devolución de llamada para que se ejecute en un momento posterior, 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 al final de la lista de tareas en cola. De forma predeterminada, scheduler.yield envía el trabajo restante al frente de la fila. Esto significa que el trabajo que querías reanudar inmediatamente después de ceder no quedará relegado a tareas de otras fuentes (con la excepción notable de las interacciones del usuario).

scheduler.yield es una función que cede el control al subproceso principal y devuelve 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 siguiente versión incorporada después 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 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á 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 genera resultados con setTimeout, puedes ver que el bucle, aunque genera resultados después de cada iteración, no envía el trabajo restante al final de la fila, sino al principio. Esto te brinda lo mejor de ambos mundos: puedes ceder para mejorar la capacidad de respuesta de entrada 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 y presiona scheduler.yield en la barra de direcciones de Chrome y selecciona Habilitar en el menú desplegable de la sección Funciones experimentales de la plataforma web.chrome://flags De esta manera, scheduler.yield (y cualquier otra función experimental) estará disponible solo en tu instancia de Chrome.
  2. Si deseas 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 los navegadores que no lo implementan, dependerá de tus objetivos. Puedes usar el polyfill oficial. El polyfill es útil si se aplica lo siguiente a tu situación:

  1. Ya utilizas scheduler.postTask en tu aplicación para programar tareas.
  2. Quieres poder establecer prioridades de tareas y de rendimiento.
  3. Quieres poder cancelar o cambiar la prioridad de las tareas con la clase TaskController que ofrece la API de scheduler.postTask.

Si esta descripción no se ajusta a tu situación, es posible que el polyfill no sea 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 tal vez supongas, los navegadores que no admiten scheduler.yield no generarán un comportamiento de "frente de la fila". Si eso significa que prefieres no ceder en absoluto, puedes probar otro enfoque que use scheduler.yield si está disponible, pero que no cederá en absoluto 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 interesante a la API del programador, que esperamos que facilite a los desarrolladores mejorar la capacidad de respuesta en comparación con las estrategias de rendimiento actuales. Si scheduler.yield te parece una API útil, participa en nuestra investigación para ayudarnos a mejorarla y proporciona comentarios sobre cómo se podría optimizar aún más.

Imagen hero de Unsplash, de Jonathan Allison.