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

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

En los últimos 20 años, la Web evolucionó de forma drástica, de documentos estáticos con algunos estilos e imágenes a aplicaciones complejas y dinámicas. Sin embargo, hay un aspecto que no ha cambiado mucho: solo tenemos un subproceso por pestaña del navegador (con algunas excepciones) para renderizar nuestros sitios y ejecutar nuestro código JavaScript.

Como resultado, el subproceso principal se sobrecargó de trabajo. A medida que las apps web se vuelven más complejas, el subproceso principal se convierte en un cuello de botella significativo para el rendimiento. Para empeorar las cosas, la cantidad de tiempo que se tarda en ejecutar el código en el subproceso principal para un usuario determinado es casi completamente impredecible porque las capacidades del dispositivo tienen un efecto enorme en el rendimiento. Esa imprevisibilidad solo aumentará a medida que los usuarios accedan a la Web desde un conjunto cada vez más diverso de dispositivos, desde teléfonos de gama media con limitaciones extremas hasta máquinas de alta gama con alta potencia y alta frecuencia de actualización.

Si queremos que las apps web sofisticadas cumplan de manera confiable con los lineamientos de rendimiento, como las Core Web Vitals, que se basan en datos empíricos sobre la percepción y la psicología humanas, necesitamos formas de ejecutar nuestro código fuera del subproceso principal (OMT).

¿Por qué usar 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 vía de escape del subproceso principal, ya que permiten que los desarrolladores creen subprocesos independientes para controlar 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 hay una cantidad considerable de trabajo que, de lo contrario, abrumaría al 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 la métrica de capacidad de respuesta de Interaction to Next Paint (INP) de una página. Cuando el subproceso principal tiene menos trabajo que procesar, puede responder con mayor rapidez a las interacciones del usuario.

Menos trabajo en el subproceso principal, en especial durante el inicio, también conlleva un beneficio potencial para el Largest Contentful Paint (LCP), ya que reduce las tareas largas. La renderización de un elemento LCP requiere tiempo del subproceso principal, ya sea para renderizar texto o imágenes, que son elementos LCP frecuentes y comunes. Si reduces el trabajo del subproceso principal en general, puedes asegurarte de que sea menos probable que el elemento LCP de tu página se bloquee por un trabajo costoso que un trabajador web podría controlar en su lugar.

Subprocesos con Web Workers

Por lo general, otras plataformas admiten el trabajo paralelo, ya que te permiten asignarle 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 mutexes y semáforos para evitar condiciones de carrera.

En JavaScript, podemos obtener una funcionalidad similar de 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 separado:

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

Comunicarte con el trabajador web enviando mensajes con la API de postMessage Pasa el valor del mensaje como un parámetro en la llamada a 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 poco limitado. Históricamente, los trabajadores web se han utilizado principalmente para quitar una sola tarea pesada del subproceso principal. Intentar controlar varias operaciones con un solo web worker se vuelve difícil de manejar rápidamente: tienes que codificar no solo los parámetros, sino también la operación en el mensaje, y tienes que llevar un registro para hacer coincidir las respuestas con las solicitudes. Es probable que esa complejidad sea el motivo por el que los trabajadores web no se hayan adoptado de forma más generalizada.

Sin embargo, si pudiéramos eliminar parte de la dificultad de la comunicación entre el subproceso principal y los trabajadores web, este modelo podría ser ideal para muchos casos de uso. Por suerte, hay una biblioteca que hace exactamente 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, impórtalo en un trabajador web y define un conjunto de funciones que se expondrán al 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 en el subproceso principal se comporta de la misma manera que la del trabajador web, excepto que cada función devuelve una promesa para 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 APIs, como WebUSB, WebRTC o Web Audio, por lo que no puedes colocar 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 libera espacio en el subproceso principal para las tareas que deben 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 en la app. Todo es un componente del framework y, por lo tanto, está vinculado de forma inherente al DOM. Eso parecería dificultar la migración a una arquitectura de OMT.

Sin embargo, si cambiamos a un modelo en el que las cuestiones de la IU se separan de otras, como la administración de estados, los trabajadores web pueden ser bastante útiles incluso con 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 un clon de Buscaminas que cumple con los requisitos de las apps web progresivas, lo que incluye funcionar sin conexión y tener una experiencia del usuario atractiva. Lamentablemente, las primeras versiones del juego tuvieron un rendimiento deficiente en dispositivos con limitaciones, 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 controla la renderización de las animaciones y las transiciones.
  • Un trabajador web controla la lógica del juego, que es puramente computacional.

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

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

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 más comentarios para el usuario. La ralentización se produce porque la app envía más fotogramas que la versión que no es de OMT, que no envía ningún fotograma. Por lo tanto, el usuario sabe que algo está sucediendo y puede seguir jugando mientras 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: les brindamos a los usuarios de dispositivos limitados 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 una gama más amplia de dispositivos, pero no la hace más rápida:

  • Solo estás moviendo el trabajo del subproceso principal, no reduciéndolo.
  • La sobrecarga de comunicación adicional entre el trabajador web y el subproceso principal a veces puede ralentizar un poco las cosas.

Considera las ventajas y desventajas

Como el subproceso principal está libre para procesar las interacciones del usuario, como el desplazamiento, mientras se ejecuta JavaScript, se pierden menos fotogramas, aunque el tiempo de espera total puede ser un poco más largo. Es preferible que el usuario espere un poco antes de descartar un fotograma, ya que el margen de error es menor para los fotogramas descartados: el descarte de un fotograma ocurre en milisegundos, mientras que tienes cientos de milisegundos antes de que un usuario perciba el tiempo de espera.

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

Nota sobre las herramientas

Los trabajadores web aún no son de uso generalizado, por lo que la mayoría de las herramientas de módulos, como webpack y Rollup, no los admiten de forma predeterminada. (Parcel sí lo hace). Por suerte, existen complementos para que los trabajadores web, bueno, trabajen 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 con limitaciones, ya que es la forma en que la mayoría de los usuarios acceden a la Web a nivel global. La OMT ofrece una forma prometedora de aumentar el rendimiento en esos dispositivos sin afectar de forma negativa a los usuarios de dispositivos de alta gama.

Además, la OMT tiene beneficios secundarios:

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

Los trabajadores web no tienen por qué ser intimidantes. Herramientas como Comlink están quitando el trabajo de los trabajadores y convirtiéndolos en una opción viable para una amplia variedad de aplicaciones web.