Mejor programación de JS con isInputPending()

Una nueva API de JavaScript que puede ayudarte a evitar la compensación entre rendimiento de carga y capacidad de respuesta de entrada.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

No es fácil cargar rápido. Los sitios que aprovechan JS para procesar el contenido actualmente deben compensar el rendimiento de la carga y la capacidad de respuesta de la entrada: realizar todo el trabajo necesario para mostrar todo a la vez (mejor rendimiento de carga y menor capacidad de respuesta de las entradas) o fragmentar el trabajo en tareas más pequeñas para mantener la capacidad de respuesta a la entrada y la pintura (peor rendimiento de carga y mejor capacidad de respuesta de entrada).

Para eliminar la necesidad de hacer esta compensación, Facebook propuso e implementó la API de isInputPending() en Chromium para mejorar la capacidad de respuesta sin generar rendimiento. En función de los comentarios sobre la prueba de origen, realizamos varias actualizaciones en la API y nos complace anunciar que la API ahora se envía de forma predeterminada en Chromium 87.

Compatibilidad del navegador

Navegadores compatibles

  • 87
  • 87
  • x
  • x

isInputPending() se enviará en navegadores basados en Chromium a partir de la versión 87. Ningún otro navegador indicó un intent para enviar la API.

Información general

En el ecosistema actual de JS, la mayor parte del trabajo se realiza en un solo subproceso: el principal. Esto proporciona un modelo de ejecución sólido a los desarrolladores, pero la experiencia del usuario (en particular, la capacidad de respuesta) puede verse afectada de forma drástica si la secuencia de comandos se ejecuta durante mucho tiempo. Si la página realiza mucho trabajo mientras se activa un evento de entrada, por ejemplo, la página no controlará el evento de entrada de clic hasta que se complete ese trabajo.

La práctica recomendada actual es abordar este problema dividiendo el JavaScript en bloques más pequeños. Mientras se carga la página, puede ejecutar un poco de JavaScript y, luego, ceder el control al navegador y transferirlo al navegador. Luego, el navegador puede verificar su cola de eventos de entrada y ver si hay algo que deba informar a la página. Luego, el navegador puede volver a ejecutar los bloques de JavaScript a medida que se agregan. Esto ayuda, pero puede causar otros problemas.

Cada vez que la página devuelve el control al navegador, este tarda un tiempo en verificar la cola de eventos de entrada, procesar los eventos y recoger el siguiente bloque de JavaScript. Si bien el navegador responde a los eventos más rápido, el tiempo de carga general de la página se ralentiza. Si lo hacemos muy a menudo, la página se carga demasiado lento. Si lo hacemos con menos frecuencia, el navegador tarda más en responder a los eventos del usuario y las personas se frustran. No es divertido.

Un diagrama que muestra que, cuando ejecutas tareas largas de JS, el navegador tiene menos tiempo para despachar los eventos.

En Facebook, queríamos ver cómo se verían las cosas si se nos ocurre un nuevo enfoque para la carga que elimine esta frustración ocasional. Nos comunicamos con nuestros amigos de Chrome sobre este tema y propusimos una propuesta para isInputPending(). La API de isInputPending() es la primera en usar el concepto de interrupciones para las entradas del usuario en la Web y permite que JavaScript pueda verificar las entradas sin ceder el paso al navegador.

Un diagrama que muestra que isInputPending() permite que tu JS compruebe si hay entradas pendientes del usuario, sin ceder la ejecución completamente al navegador.

Dado que había interés en la API, nos asociamos con nuestros colegas de Chrome para implementar y lanzar la función en Chromium. Con la ayuda de los ingenieros de Chrome, logramos implementar los parches en una prueba de origen (una forma en la que Chrome prueba cambios y recibe comentarios de los desarrolladores antes de lanzar una API por completo).

Tomamos comentarios de la prueba de origen y de los otros miembros del Grupo de trabajo de rendimiento web de W3C e implementamos cambios en la API.

Ejemplo: un programador de yieldier

Supongamos que tienes que hacer un montón de trabajo de bloqueo de visualización para cargar tu página, por ejemplo, generar lenguaje de marcado a partir de componentes, excluir números primos o simplemente dibujar un ícono giratorio de carga en frío. Cada uno de ellos se divide en un elemento de trabajo discreto. Con el patrón del programador, esbozamos cómo podríamos procesar nuestro trabajo en una función processWorkQueue() hipotética:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Si invocas processWorkQueue() más adelante en una macrotarea nueva a través de setTimeout(), le otorgamos al navegador la capacidad de mantener una capacidad de respuesta a las entradas (puede ejecutar controladores de eventos antes de que se reanude el trabajo) y, al mismo tiempo, administrar la ejecución relativamente sin interrupciones. Sin embargo, es posible que otros trabajos que deseen controlar el bucle de eventos demoren mucho tiempo o que alcancen hasta QUANTUM milisegundos adicionales de latencia de evento.

Esto está bien, pero ¿podemos hacerlo mejor? ¡Por supuesto!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Si ingresamos una llamada a navigator.scheduling.isInputPending(), podemos responder a la entrada más rápido y, a la vez, asegurarnos de que nuestro trabajo de bloqueo de la pantalla se ejecute sin interrupciones. Si no nos interesa controlar nada más que la entrada (p.ej., pintura) hasta que el trabajo esté completo, también podemos aumentar fácilmente la longitud de QUANTUM.

De forma predeterminada, no se muestran eventos "continuos" desde isInputPending(). Estos incluyen mousemove, pointermove y otros. Si también te interesa rendir homenaje para estos también, no hay problema. Si proporcionas un objeto a isInputPending() con includeContinuous establecido en true, podemos continuar:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Listo. Los frameworks como React están compilando la compatibilidad con isInputPending() en sus bibliotecas de programación principales con una lógica similar. Se espera que los desarrolladores que usan estos frameworks se beneficien de isInputPending() en segundo plano sin reescrituras significativas.

No siempre es malo cosechar

Ten en cuenta que producir menos no es la solución adecuada para todos los casos de uso. Existen muchas razones para devolver el control al navegador además de procesar eventos de entrada, como realizar la renderización y ejecutar otras secuencias de comandos en la página.

Existen casos en los que el navegador no puede atribuir correctamente eventos de entrada pendientes. En particular, configurar clips y máscaras complejos para iframes de origen cruzado puede informar falsos negativos (es decir, isInputPending() puede mostrar un resultado falso de forma inesperada cuando se orienta a estos fotogramas). Asegúrate de que el rendimiento sea lo suficientemente frecuente si tu sitio requiere interacciones con submarcos estilizados.

Además, ten en cuenta las demás páginas que comparten un bucle de eventos. En plataformas como Chrome para Android, es bastante común que varios orígenes compartan un bucle de eventos. isInputPending() nunca mostrará true si la entrada se envía a un marco de origen cruzado y, por lo tanto, las páginas en segundo plano pueden interferir con la capacidad de respuesta de las páginas en primer plano. Es posible que quieras reducir, posponer o producir con más frecuencia cuando trabajas en segundo plano con la API de visibilidad de páginas.

Te recomendamos que uses isInputPending() a discreción. Si no hay trabajo de bloqueo de usuarios por hacer, sé amable con los demás en el bucle de eventos mediante el rendimiento con mayor frecuencia. Las tareas largas pueden ser dañinas.

Comentarios

  • Deja comentarios sobre la especificación en el repositorio is-input-pending.
  • Comunícate con @acomminos (uno de los autores de especificaciones) en Twitter.

Conclusión

Nos complace el lanzamiento de isInputPending() y que los desarrolladores puedan comenzar a usarlo hoy mismo. Esta API es la primera vez que Facebook compiló una nueva API web y la llevó de la incubación de ideas a la propuesta de estándares y la envío a un navegador. Queremos agradecer a todos los que nos ayudaron a llegar a este punto y saludar de forma especial a todos los de Chrome que nos ayudaron a dar forma a esta idea y a enviarla.

Foto hero de Will H McMahan en Unsplash.