Articles

Enhebrar la web con trabajadores de módulos

JavaScript es un subproceso único, lo que significa que solo puede realizar una operación a la vez. Esto es intuitivo y funciona bien para muchos casos en la web, pero puede volverse problemático cuando necesitamos realizar tareas pesadas como procesamiento de datos, análisis, computación o análisis. A medida que se entregan más y más aplicaciones complejas en la web, hay una mayor necesidad de procesamiento de subprocesos múltiples.

En la plataforma web, el primitivo principal para el subproceso y el paralelismo es la API de Web Workers. Los trabajadores son una abstracción ligera en la parte superior de los subprocesos del sistema operativo que exponen una API de paso de mensajes para la comunicación entre subprocesos. Esto puede ser inmensamente útil cuando se realizan cálculos costosos o se opera en grandes conjuntos de datos, lo que permite que el subproceso principal se ejecute sin problemas mientras se realizan las costosas operaciones en uno o más subprocesos en segundo plano.

Este es un ejemplo típico de uso de worker, donde un script de worker escucha mensajes del hilo principal y responde enviando mensajes propios: página

.js:

const worker = new Worker('worker.js');
worker.addEventListener(e => {
console.log(e.data);
});
worker.postMessage('hello');

trabajador.js:

addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});

La API de trabajo Web ha estado disponible en la mayoría de los navegadores durante más de diez años. Si bien eso significa que los trabajadores tienen un excelente soporte para navegadores y están bien optimizados, también significa que son anteriores a los módulos JavaScript. Dado que no había un sistema de módulos cuando se diseñaron los workers, la API para cargar código en un worker y componer scripts se ha mantenido similar a los enfoques de carga de scripts síncronos comunes en 2009.

Historial: trabajadores clásicos #

El constructor de trabajadores toma una URL de script clásica, que es relativa a la URL del documento. Devuelve inmediatamente una referencia a la nueva instancia de worker, que expone una interfaz de mensajería, así como un método terminate() que detiene y destruye inmediatamente el worker.

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

Una función importScripts() está disponible dentro de web workers para cargar código adicional, pero detiene la ejecución del worker para obtener y evaluar cada script. También ejecuta scripts en el ámbito global como una etiqueta clásica <script>, lo que significa que las variables de un script pueden ser sobrescritas por las variables de otro.

trabajador.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
return 'world';
}

Por esta razón, los trabajadores web han impuesto históricamente un efecto desmesurado en la arquitectura de una aplicación. Los desarrolladores han tenido que crear herramientas inteligentes y soluciones para hacer posible el uso de trabajadores web sin renunciar a las prácticas de desarrollo modernas. Por ejemplo, los paquetes como webpack incrustan una pequeña implementación de cargador de módulos en el código generado que utiliza importScripts() para cargar código, pero envuelve los módulos en funciones para evitar colisiones variables y simular importaciones y exportaciones de dependencias.

Enter module workers #

Un nuevo modo para trabajadores web con las ventajas ergonómicas y de rendimiento de los módulos JavaScript está disponible en Chrome 80, llamado module workers. El constructor Worker ahora acepta una nueva opción {type:"module"}, que cambia la carga y ejecución del script para que coincida con <script type="module">.

const worker = new Worker('worker.js', {
type: 'module'
});

Dado que los trabajadores de módulos son módulos estándar de JavaScript, pueden usar instrucciones de importación y exportación. Al igual que con todos los módulos JavaScript, las dependencias solo se ejecutan una vez en un contexto determinado (hilo principal, trabajador, etc.), y todas las importaciones futuras hacen referencia a la instancia de módulo ya ejecutada. La carga y ejecución de módulos JavaScript también está optimizada por los navegadores. Las dependencias de un módulo se pueden cargar antes de que se ejecute el módulo, lo que permite cargar árboles de módulos completos en paralelo. La carga de módulos también almacena en caché el código analizado, lo que significa que los módulos que se utilizan en el subproceso principal y en un trabajo solo deben analizarse una vez.

Pasar a los módulos JavaScript también permite el uso de importación dinámica para cargar código sin bloquear la ejecución del trabajador. La importación dinámica es mucho más explícita que usar importScripts() para cargar dependencias, ya que las exportaciones del módulo importado se devuelven en lugar de depender de variables globales.

trabajador.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}

Para garantizar un gran rendimiento, el antiguo método importScripts() no está disponible en los trabajadores del módulo. Cambiar trabajadores para usar módulos JavaScript significa que todo el código se carga en modo estricto. Otro cambio notable es que el valor de this en el ámbito de nivel superior de un módulo JavaScript es undefined, mientras que en los trabajadores clásicos el valor es el ámbito global del trabajador. Afortunadamente, siempre ha habido unself global que proporciona una referencia al alcance global. Está disponible en todo tipo de trabajadores, incluidos los trabajadores de servicios, así como en el DOM.

Los trabajadores del módulo también eliminan el soporte para comentarios de estilo HTML. ¿Sabía que podía usar comentarios HTML en scripts de trabajo web?

Trabajadores de precarga con modulepreload #

Una mejora sustancial del rendimiento que viene con los trabajadores de módulos es la capacidad de precarga de los trabajadores y sus dependencias. Con module workers, los scripts se cargan y ejecutan como módulos JavaScript estándar, lo que significa que se pueden precargar e incluso analizar previamente utilizando modulepreload:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">
<script>
addEventListener('load', () => {
// our worker code is likely already parsed and ready to execute!
const worker = new Worker('worker.js', { type: 'module' });
});
</script>

Los módulos precargados también pueden ser utilizados por los trabajadores del subproceso principal y del módulo. Esto es útil para módulos que se importan en ambos contextos, o en casos en los que no es posible saber de antemano si un módulo se utilizará en el subproceso principal o en un trabajo.

Anteriormente, las opciones disponibles para la precarga de scripts de trabajo web eran limitadas y no necesariamente confiables. Los trabajadores clásicos tenían su propio tipo de recurso» trabajador»para la precarga, pero ningún navegador implementó <link rel="preload" as="worker">. Como resultado, la técnica principal disponible para la precarga de los trabajadores web era usar <link rel="prefetch">, que dependía completamente de la caché HTTP. Cuando se usa en combinación con los encabezados de caché correctos, esto hizo posible evitar que la creación de instancias de trabajo tuviera que esperar para descargar el script de trabajo. Sin embargo, a diferencia de modulepreload esta técnica no soportaba la precarga de dependencias ni el análisis previo.

¿Qué pasa con los trabajadores compartidos? #

Los trabajadores compartidos se han actualizado con soporte para módulos JavaScript a partir de Chrome 83. Al igual que los trabajadores dedicados, la construcción de un trabajador compartido con la opción {type:"module"} ahora carga el script de trabajo como un módulo en lugar de un script clásico:

const worker = new SharedWorker('/worker.js', {
type: 'module'
});

Antes de admitir módulos JavaScript, el constructor SharedWorker() solo esperaba una URL y un . Esto seguirá funcionando para el uso clásico de trabajadores compartidos; sin embargo, la creación de trabajadores compartidos de módulos requiere el uso del nuevo argumento options. Las opciones disponibles son las mismas que las de un trabajador dedicado, incluida la opción name que reemplaza al argumento anterior name.

¿Qué pasa con el trabajador de servicio? #

La especificación del trabajador de servicio ya se ha actualizado para admitir la aceptación de un módulo JavaScript como punto de entrada, utilizando la misma opción {type:"module"} como trabajadores de módulo, sin embargo, este cambio aún no se ha implementado en los navegadores. Una vez que esto suceda, será posible crear una instancia de un trabajador de servicio utilizando un módulo JavaScript utilizando el siguiente código:

navigator.serviceWorker.register('/sw.js', {
type: 'module'
});

Ahora que se ha actualizado la especificación, los navegadores están comenzando a implementar el nuevo comportamiento. Esto lleva tiempo porque hay algunas complicaciones adicionales asociadas con traer módulos JavaScript a service worker. El registro de Service worker debe comparar los scripts importados con sus versiones en caché anteriores al determinar si se activa una actualización, y esto debe implementarse para los módulos JavaScript cuando se usa para service workers. Además, los trabajadores de servicio deben poder omitir la caché de scripts en ciertos casos al verificar actualizaciones.