Usa Web workers para ejecutar JavaScript desde el subproceso principal del navegador.

Una arquitectura fuera del subproceso principal puede mejorar significativamente la confiabilidad de tu app y la experiencia del usuario.

Surma
Surma

En los últimos 20 años, la Web ha evolucionado drásticamente desde documentos estáticos con algunos estilos e imágenes hasta aplicaciones complejas y dinámicas. Sin embargo, hubo algo que se mantuvo en gran medida: solo tenemos un subproceso por pestaña del navegador (con algunas excepciones) para procesar nuestros sitios y ejecutar JavaScript.

Como resultado, el subproceso principal se sobrecargó. A medida que aumenta la complejidad de las aplicaciones web, el subproceso principal se convierte en un importante cuello de botella para el rendimiento. Y, peor aún, la cantidad de tiempo que lleva ejecutar código en el subproceso principal de un usuario determinado es casi impredecible, ya que las capacidades del dispositivo tienen un efecto masivo en el rendimiento. Esa imprevisibilidad solo aumentará a medida que los usuarios accedan a la Web desde un conjunto de dispositivos cada vez más diverso, desde teléfonos de gama media con hiperrestringidos hasta equipos insignia de alta potencia y frecuencia de actualización.

Si queremos que las aplicaciones web sofisticadas cumplan de forma confiable con los lineamientos de rendimiento, como las Métricas web esenciales, que se basan en datos empíricos sobre la percepción humana y la psicología, necesitamos formas de ejecutar nuestro código desde el subproceso principal (OMT).

¿Por qué elegir los trabajadores web?

De forma predeterminada, JavaScript es un lenguaje de un solo subproceso que ejecuta tareas en el subproceso principal. Sin embargo, los trabajadores web proporcionan una especie de salida del subproceso principal, ya que permiten a los desarrolladores iniciar subprocesos separados para manejar el trabajo fuera del subproceso principal. Si bien el alcance de los trabajadores web es limitado y no ofrece acceso directo al DOM, pueden ser muy beneficiosos si se necesita realizar una cantidad considerable de trabajo que, de lo contrario, sobrecargaría el subproceso principal.

En lo que respecta a las Métricas web esenciales, puede ser beneficioso ejecutar el trabajo fuera del subproceso principal. En particular, descargar el trabajo del subproceso principal a los trabajadores web puede reducir la contención del subproceso principal, lo que puede mejorar las métricas de capacidad de respuesta importantes, como la Interacción a la siguiente pintura (INP) y el Retraso de primera entrada (FID). Cuando el subproceso principal tiene menos trabajo para procesar, puede responder más rápido a las interacciones del usuario.

Un menor trabajo en el subproceso principal, especialmente durante el inicio, también tiene un posible beneficio para el Largest Contentful Paint (LCP), ya que reduce las tareas largas. La renderización de un elemento LCP requiere tiempo de subproceso principal, ya sea para renderizar texto o imágenes, que son elementos LCP frecuentes y comunes, y al reducir el trabajo del subproceso principal en general, puedes asegurarte de que el elemento LCP de tu página tenga menos probabilidades de que se bloquee por tareas costosas que un trabajador web pueda manejar en su lugar.

Cómo ejecutar subprocesos con trabajadores web

Por lo general, otras plataformas admiten el trabajo paralelo, ya que te permiten asignar una función a un subproceso, que se ejecuta en paralelo con el resto del programa. Puedes acceder a las mismas variables desde ambos subprocesos y el acceso a estos recursos compartidos se puede sincronizar con exclusiones mutuas y semáforas para evitar condiciones de carrera.

En JavaScript, podemos obtener una funcionalidad más o menos similar a los trabajadores web, que existen desde 2007 y son compatibles con todos los navegadores principales desde 2012. Los trabajadores web se ejecutan en paralelo con el subproceso principal, pero, a diferencia de los subprocesos del SO, no pueden compartir variables.

Para crear un trabajador web, pasa un archivo al constructor del trabajador, que comienza a ejecutar ese archivo en un subproceso independiente:

const worker = new Worker("./worker.js");

Envíe mensajes a través de la API de postMessage para comunicarse con el trabajador web. Pasa el valor del mensaje como parámetro en la llamada postMessage y, luego, agrega un objeto de escucha de eventos de mensaje al trabajador:

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

Para enviar un mensaje de vuelta al subproceso principal, usa la misma API de postMessage en el trabajador web y configura un objeto de escucha de eventos en el subproceso principal:

main.js

const worker = new Worker('./worker.js');

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

Es cierto que este enfoque es un tanto limitado. Históricamente, los trabajadores web se usaron principalmente para quitar una sola pieza de trabajo pesado del subproceso principal. Intentar manejar varias operaciones con un solo trabajador web se vuelve difícil de manejar rápidamente: hay que codificar no solo los parámetros, sino también la operación en el mensaje, y hay que llevar una contabilidad para hacer coincidir las respuestas con las solicitudes. Esa complejidad probablemente se debe al motivo por el cual los trabajadores web no se han adoptado de forma más generalizada.

Sin embargo, si pudiéramos quitar parte de las dificultades de la comunicación entre el subproceso principal y los trabajadores web, este modelo podría ser una gran opción para muchos casos de uso. Por suerte, hay una biblioteca que se encarga de eso.

Comlink es una biblioteca cuyo objetivo es permitirte usar trabajadores web sin tener que pensar en los detalles de postMessage. Comlink te permite compartir variables entre los trabajadores web y el subproceso principal, casi como otros lenguajes de programación que admiten subprocesos.

Para configurar Comlink, debe importarlo en un trabajador web y definir un conjunto de funciones para exponer en el subproceso principal. Luego, importa Comlink en el subproceso principal, une el trabajador y obtén acceso a las funciones expuestas:

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

La variable api del subproceso principal se comporta de la misma manera que la del trabajador web, excepto que cada función muestra una promesa de un valor en lugar del valor en sí.

¿Qué código deberías mover a un trabajador web?

Los trabajadores web no tienen acceso al DOM ni a muchas API, como WebUSB, WebRTC o Web Audio, por lo que no puedes incluir partes de tu app que dependan de ese acceso en un trabajador. Aun así, cada pequeño fragmento de código que se traslada a un trabajador compra más espacio en el subproceso principal para las cosas que necesitan estar allí, como actualizar la interfaz de usuario.

Un problema para los desarrolladores web es que la mayoría de las apps web dependen de un framework de IU, como Vue o React, para organizar todo el contenido de la app; todo es un componente del framework y, por lo tanto, está inherentemente vinculado al DOM. Eso podría dificultar la migración a una arquitectura OMT.

Sin embargo, si cambiamos a un modelo en el que los problemas de la IU estén separados de otros, como la administración del estado, los trabajadores web pueden ser bastante útiles incluso con las apps basadas en frameworks. Ese es exactamente el enfoque adoptado con PROXX.

PROXX: un caso de éxito de OMT

El equipo de Google Chrome desarrolló PROXX como una clonación del Buscaminas que cumple con los requisitos de las apps web progresivas, lo que incluye trabajar sin conexión y ofrecer una experiencia del usuario atractiva. Lamentablemente, las primeras versiones del juego tuvieron un rendimiento deficiente en dispositivos limitados, como los teléfonos de gama media, lo que llevó al equipo a darse cuenta de que el subproceso principal era un cuello de botella.

El equipo decidió usar trabajadores web para separar el estado visual del juego de su lógica:

  • El subproceso principal se encarga de la renderización de animaciones y transiciones.
  • Un trabajador web maneja la lógica del juego, que es puramente computacional.

OMT tuvo efectos interesantes en el rendimiento de los teléfonos de gama media de PROXX. En la versión sin OMT, la IU se bloquea durante seis segundos después de que el usuario interactúa con ella. No hay retroalimentación y el usuario debe esperar los seis segundos completos antes de poder hacer otra cosa.

Tiempo de respuesta de la IU en la versión de PROXX no OMT.

Sin embargo, en la versión de OMT, el juego tarda doce segundos en completar una actualización de la IU. Si bien eso parece una pérdida de rendimiento, en realidad genera una mayor cantidad de comentarios para el usuario. La demora se produce porque la app envía más fotogramas que la versión que no es OMT, que no envía ningún fotograma. Por lo tanto, el usuario sabe que algo está sucediendo y puede seguir jugando a medida que se actualiza la IU, lo que hace que el juego se sienta mucho mejor.

Tiempo de respuesta de la IU en la versión de OMT de PROXX.

Esta es una compensación consciente: brindamos a los usuarios de dispositivos restringidos una experiencia que se siente mejor sin penalizar a los usuarios de dispositivos de alta gama.

Implicaciones de una arquitectura de OMT

Como se muestra en el ejemplo de PROXX, OMT hace que tu app se ejecute de manera confiable en un rango más amplio de dispositivos, pero no hace que sea más rápida:

  • Solo se mueve el trabajo del subproceso principal, no se reduce.
  • La sobrecarga de comunicación adicional entre el trabajador web y el subproceso principal a veces puede hacer que el proceso sea marginalmente más lento.

Consideraciones

Dado que el subproceso principal es libre para procesar las interacciones del usuario, como el desplazamiento mientras se ejecuta JavaScript, se descartan menos fotogramas, a pesar de que el tiempo de espera total puede ser marginalmente mayor. Es preferible hacer que el usuario espere un poco a descartar un fotograma porque el margen de error es menor para los fotogramas descartados: este proceso se realiza en milisegundos, mientras que tienes cientos de milisegundos antes de que el usuario perciba el tiempo de espera.

Debido a la imprevisibilidad del rendimiento en todos los dispositivos, el objetivo de la arquitectura OMT es reducir el riesgo, es decir, hacer que tu app sea más sólida frente a condiciones de tiempo de ejecución muy variables, no por los beneficios de rendimiento de la paralelización. El aumento de la resiliencia y las mejoras en la UX vale más que cualquier pequeño sacrificar la velocidad.

Nota sobre las herramientas

Los trabajadores web aún no son comunes, por lo que la mayoría de las herramientas de los módulos, como webpack y Rollup, no son compatibles de inmediato. (Parcel sí lo hace). Por suerte, existen complementos que permiten que los trabajadores web funcionen con webpack y Rollup:

En resumen

Para asegurarnos de que nuestras apps sean lo más confiables y accesibles posible, especialmente en un mercado cada vez más globalizado, debemos admitir dispositivos limitados, que son la forma en que la mayoría de los usuarios acceden a la Web en todo el mundo. OMT ofrece una forma prometedora de aumentar el rendimiento en esos dispositivos sin afectar negativamente a los usuarios de dispositivos de alta gama.

Además, OMT tiene beneficios secundarios:

  • Mueve los costos de ejecución de JavaScript a un subproceso independiente.
  • Reduce los costos de los análisis, lo que significa que la IU podría iniciarse más rápido. Eso podría reducir el First Contentful Paint o incluso el tiempo de carga, lo que, a su vez, puede aumentar tu puntuación de Lighthouse.

Los trabajadores web no tienen por qué ser aterradores. Las herramientas como Comlink les quitan el trabajo a los trabajadores y los convierten en una opción viable para una amplia gama de aplicaciones web.

Hero image de Unsplash, de James Peacock.