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. Justo este año, se anunció que la métrica Interacción a la siguiente pintura (INP) pasaría del estado experimental a pendiente. Ahora se espera que reemplace 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 hacer 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. 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.

Rendimiento

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, se ejecuta 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.

Además 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á, es solo 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 deficiente de la página porque retrasan la capacidad del navegador para responder a la entrada del usuario. Cuanto más a menudo se producen las tareas largas, y mientras más se ejecuten, 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, el hecho de que tu código inicie una tarea en el navegador no significa que tengas que esperar hasta que la tarea se complete antes de devolver el control al subproceso principal. Para mejorar la capacidad de respuesta a la entrada del usuario en una página, se genera explícitamente una tarea, lo que divide la tarea para que se complete en la siguiente oportunidad disponible. Esto permite que otras tareas obtengan tiempo en el subproceso principal más rápido 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 a las entradas. En la parte superior, una tarea larga bloquea la ejecución de un controlador de eventos hasta que finaliza la tarea. En la parte inferior, la tarea fragmentada permite que el controlador de eventos se ejecute antes de lo esperado.
Una visualización de ceder el control al subproceso principal. En la parte superior, el rendimiento ocurre solo después de que una tarea se ejecuta hasta su finalización, lo que significa que las tareas pueden tardar más en completarse antes de devolver el control al subproceso principal. En la parte inferior, el rendimiento se realiza de manera explícita, es decir, se divide una tarea larga en varias tareas más pequeñas. Esto permite que las interacciones del usuario se ejecuten más rápido, lo que mejora la capacidad de respuesta a la entrada y el INP.

Cuando cedas de forma explícita, le dices al navegador "Hola, entiendo que el trabajo que estoy por hacer podría llevar tiempo y no quiero que tengas que hacer todo ese trabajo antes de responder a la entrada del usuario u 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 de rendimiento de 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 rinda por su cuenta, indicas “dividamos esta gran parte del 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 interacciones de usuarios seguirán yendo al principio de la cola como deberían, pero el trabajo restante que querías hacer después de mostrar datos de forma explícita podría terminar con un retraso aún mayor por otras tareas de fuentes rivales que se pusieron en cola antes que él.

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 superior etiquetado como Ejecutar tareas periódicamente, el cual programará las tareas de bloqueo para que se ejecuten de vez en cuando. 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. A continuación, haz clic en el botón Run loop, que rinde con setTimeout en cada iteración.

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

En este resultado, se muestra el “final de la lista de tareas en cola” Comportamiento que se produce cuando se procesa el rendimiento con setTimeout El bucle que ejecuta procesa cinco elementos y rinde 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 (especialmente una de terceros) registre una función de temporizador que ejecuta trabajo en algún intervalo. El “fin de la lista de tareas en cola” El comportamiento 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. Generar un rendimiento es bueno porque las interacciones del usuario tienen la oportunidad de ejecutarse más rápido, pero también permite que otras tareas de interacción que no sean del usuario obtengan tiempo en el subproceso principal. Es un verdadero problema, pero scheduler.yield puede ayudar a resolverlo.

Ingresa scheduler.yield.

scheduler.yield está disponible detrás de una marca como 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 rendimiento 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 al 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 se convertirá en tareas de otras fuentes (excepto las interacciones de los usuarios).

scheduler.yield es una función que cede al subproceso principal y muestra un Promise cuando se lo 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 su versión incorporada debajo de esta lista.
  4. Haz clic en el botón superior con la etiqueta Ejecutar tareas de forma periódica.
  5. Por último, haz clic en el botón Run loop, que rinde con scheduler.yield en cada iteración.

El resultado en el cuadro ubicado 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 genera con setTimeout, puedes ver que el bucle, aunque rinde después de cada iteración, no envía el trabajo restante al fondo de la cola, sino al principio de ella. Esto te da lo mejor de ambos mundos: puedes mejorar la capacidad de respuesta de la entrada en tu sitio web, pero también asegurarte de que el trabajo que querías terminar después de generar 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. Esta acción 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 brinda al equipo de Chrome información valiosa 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 manera en que uses scheduler.yield (y, al mismo tiempo, admitas navegadores que no lo implementan) dependerá de cuáles sean 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 tareas y obtener prioridades.
  3. Deseas poder cancelar o volver a priorizar tareas a través de la clase TaskController que ofrece la API de scheduler.postTask.

Si no se describe tu situación, es posible que el polyfill no sea adecuado para ti. En ese caso, puedes revertir tu propio resguardo de dos 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 te imaginarás, los navegadores que no sean compatibles con scheduler.yield mostrarán el contenido sin "primera fila". el comportamiento de los usuarios. Si eso significa que prefieres no ceder, puedes probar otro enfoque que use scheduler.yield si está disponible, pero que no rinde 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 emocionante a la API del programador, que permitirá que los desarrolladores mejoren 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 mejorarla y envíanos comentarios sobre cómo se puede mejorar.

Hero image de Unsplash, de Jonathan Allison.