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.
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 realizar esta compensación, Facebook propuso e implementó la API de isInputPending()
en Chromium para mejorar la capacidad de respuesta sin generar rendimientos. 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
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. Luego, el navegador puede verificar su cola de eventos de entrada y ver si hay algo que deba decirle 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.
En Facebook, queríamos ver cómo sería todo si se ideara un enfoque nuevo para la carga que eliminara esta compensación frustrante. Nos comunicamos con nuestros amigos de Chrome al respecto y elaboramos la 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.
Como 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 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).
Ahora, tenemos en cuenta los comentarios de la prueba de origen y de los otros miembros del grupo de trabajo de rendimiento web del W3C, y también implementamos cambios en la API.
Ejemplo: un programador más eficiente
Supongamos que tienes mucho trabajo de bloqueo de pantalla para cargar tu página, por ejemplo, generar marcado a partir de componentes, factorizar números primos o simplemente dibujar un ícono de carga genial. Cada uno de ellos 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 otro trabajo que quiera controlar el bucle de eventos nos desprograme 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, no se muestran eventos "continuos" desde isInputPending()
. 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
, todo estará 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.
Ceder 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 que otras páginas también 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, por lo que 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 trabajo de bloqueo del usuario que se deba realizar, cede con más frecuencia a los demás en el bucle de eventos. 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 es la primera vez que Facebook crea una API web nueva y la lleva de la incubación de ideas a la propuesta de estándares para su 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.