Mejor programación de JS con isInputPending()

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Cargar rápido es difícil. Actualmente, los sitios que aprovechan JS para renderizar su contenido deben realizar una compensación entre el rendimiento de la carga y la capacidad de respuesta de la entrada: realizar todo el trabajo necesario para la visualización de una sola vez (mejor rendimiento de la carga, peor capacidad de respuesta de la entrada) o dividir el trabajo en tareas más pequeñas para seguir siendo responsivo a la entrada y la pintura (peor rendimiento de la carga, mejor capacidad de respuesta de la 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 errores. En función de los comentarios de la prueba de origen, realizamos varias actualizaciones de la API y nos complace anunciar que ahora se envía de forma predeterminada en Chromium 87.

Compatibilidad del navegador

Navegadores compatibles

  • Chrome: 87.
  • Edge: 87.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

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

Segundo plano

La mayor parte del trabajo en el ecosistema de JS actual se realiza en un solo subproceso: el subproceso 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 un período prolongado. Por ejemplo, si la página realiza muchas tareas mientras se activa un evento de entrada, esta no controlará el evento de entrada de clic hasta que se complete esa tarea.

La práctica recomendada actual para abordar este problema es dividir el código JavaScript en bloques más pequeños. Mientras se carga la página, esta puede ejecutar un fragmento de JavaScript y, luego, ceder y pasar el control al navegador. El navegador puede verificar su cola de eventos de entrada y ver si hay algo sobre lo 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 le cede el control al navegador, este tarda un poco en verificar su cola de eventos de entrada, procesar los eventos y retomar 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. Y si lo hacemos con demasiada frecuencia, la página 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 de JS largas, el navegador tiene menos tiempo para enviar eventos.

En Facebook, queríamos saber cómo sería si propusiéramos un nuevo enfoque de carga que eliminara esta frustrante compensación. Nos comunicamos con nuestros amigos de Chrome para hablar sobre esto y proponemos 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 al navegador.

Un diagrama que muestra que isInputPending() permite que tu código JS verifique si hay entradas del usuario pendientes, sin devolver la ejecución por completo al navegador.

Como hubo interés en la API, nos asociamos con nuestros colegas de Chrome para implementar y publicar la función en Chromium. Con la ayuda de los ingenieros de Chrome, logramos que los parches se lanzaran después de una prueba de origen (que es una forma en que Chrome prueba los cambios y recibe comentarios de los desarrolladores antes de lanzar una API por completo).

Ya tomamos los comentarios de la prueba de origen y de los otros miembros del grupo de trabajo de rendimiento web del W3C, y, además, implementamos cambios en la API.

Ejemplo: un programador más eficiente

Supongamos que tienes que realizar una gran cantidad de trabajo para bloquear la pantalla, como generar lenguaje de marcado a partir de componentes, excluir números primos o solo dibujar un ícono giratorio de carga genial. Cada una de ellas se divide en un elemento de trabajo discreto. Con el patrón de programador, esbocemos cómo podríamos procesar nuestro trabajo en una función hipotética processWorkQueue():

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();
}

Cuando invocamos processWorkQueue() más adelante en una macrotarea nueva a través de setTimeout(), le brindamos al navegador la capacidad de seguir siendo un poco responsivo a la entrada (puede ejecutar controladores de eventos antes de que se reanude el trabajo) y, al mismo tiempo, logra ejecutarse de forma relativamente ininterrumpida. Sin embargo, es posible que otros trabajos que requieran el control del bucle de eventos nos desprogramen durante mucho tiempo o que obtengamos hasta QUANTUM milisegundos adicionales de latencia del evento.

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();
}

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

De forma predeterminada, isInputPending() no muestra eventos "continuos". Estos incluyen mousemove, pointermove y otros. Si también te interesa ceder estos datos, no hay problema. Si proporcionas un objeto a isInputPending() con includeContinuous configurado como true, estará todo listo:

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();
}

Eso es todo. Frameworks como React están compilando compatibilidad con isInputPending() en sus bibliotecas de programación principales con una lógica similar. Con suerte, esto llevará a que los desarrolladores que usan estos frameworks puedan beneficiarse de isInputPending() en segundo plano sin reescrituras significativas.

Rindar no siempre es malo

Vale la pena señalar que producir menos no es la solución adecuada para todos los casos de uso. Existen muchos motivos para devolver el control al navegador, además de procesar eventos de entrada, como para 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 los 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 valor falso de forma inesperada cuando se segmenta para estos marcos). Asegúrate de generar con la frecuencia suficiente si tu sitio requiere interacciones con submarcos estilizados.

Ten en cuenta también otras 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. Te recomendamos que reduzcas, pospongas o cedas con más frecuencia cuando realices tareas en segundo plano con la API de Page Visibility.

Te recomendamos que uses isInputPending() con discreción. Si no hay que hacer un trabajo para bloquear a los usuarios, sé amable con los demás participantes del bucle de eventos y rinde con más frecuencia. Las tareas largas pueden ser dañinas.

Comentarios

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

Conclusión

Nos complace que se lance 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 estándar, al envío en un navegador. Queremos agradecer a todos los que nos ayudaron a llegar hasta este punto y hacer una mención especial a todos los miembros del equipo de Chrome que nos ayudaron a desarrollar esta idea y llevarla a cabo.

Foto hero de Will H McMahan en Unsplash.