Ahora es más fácil mover el trabajo pesado a los subprocesos en segundo plano con los módulos de JavaScript en los trabajadores web.
JavaScript es de un solo subproceso, 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 el procesamiento de datos, el análisis, el cálculo o el análisis. A medida que se entregan aplicaciones cada vez más complejas en la Web, aumenta la necesidad de procesamiento de varios subprocesos.
En la plataforma web, la primitiva principal para los subprocesos y el paralelismo es la API de Web Workers. Los trabajadores son una abstracción ligera sobre los subprocesos del sistema operativo que exponen una API de paso de mensajes para la comunicación entre subprocesos. Esto puede ser muy útil cuando se realizan cálculos costosos o se opera con grandes conjuntos de datos, ya que permite que el subproceso principal se ejecute sin problemas mientras se realizan las operaciones costosas en uno o más subprocesos en segundo plano.
Este es un ejemplo típico de uso de un trabajador, en el que una secuencia de comandos de trabajador escucha los mensajes del subproceso principal y responde enviando sus propios mensajes:
page.js:
const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
console.log(e.data);
});
worker.postMessage('hello');
worker.js:
addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});
La API de Web Worker está disponible en la mayoría de los navegadores desde hace más de diez años. Si bien eso significa que los trabajadores tienen una excelente compatibilidad con los navegadores y están bien optimizados, también significa que son muy anteriores a los módulos de JavaScript. Como no había un sistema de módulos cuando se diseñaron los trabajadores, la API para cargar código en un trabajador y componer secuencias de comandos se mantuvo similar a los enfoques de carga de secuencias de comandos síncronas comunes en 2009.
Historial: Trabajadores clásicos
El constructor de Worker toma una URL de secuencia de comandos clásica, que es relativa a la URL del documento. Devuelve de inmediato una referencia a la nueva instancia de trabajador,
que expone una interfaz de mensajería y un método terminate() que detiene y
destruye al trabajador de inmediato.
const worker = new Worker('worker.js');
Hay una función importScripts() disponible dentro de los trabajadores web para cargar código adicional, pero
pausa la ejecución del trabajador para recuperar y evaluar cada secuencia de comandos. También ejecuta secuencias de comandos en el alcance global, como una etiqueta <script> clásica, lo que significa que las variables de una secuencia de comandos se pueden reemplazar por las variables de otra.
worker.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 este motivo, los trabajadores web históricamente han impuesto un efecto desmesurado en la arquitectura de una
aplicación. Los desarrolladores tuvieron que crear herramientas y soluciones alternativas inteligentes para poder usar trabajadores web sin renunciar a las prácticas de desarrollo modernas. Por ejemplo, los agrupadores como
webpack incorporan una pequeña implementación de cargador de módulos en el código generado que usa importScripts()
para la carga de código, pero une los módulos en funciones para evitar colisiones de variables y simular
importaciones y exportaciones de dependencias.
Ingresa los trabajadores del módulo
En Chrome 80, se incluye un nuevo modo para los trabajadores web con los beneficios de ergonomía y rendimiento de los módulos de JavaScript, llamados trabajadores de módulos. El constructor Worker ahora acepta una nueva opción {type:"module"}, que cambia la carga y la ejecución de la secuencia de comandos para que coincida con <script type="module">.
const worker = new Worker('worker.js', {
type: 'module'
});
Como los trabajadores de módulos son módulos de JavaScript estándar, pueden usar sentencias de importación y exportación. Al igual que con todos los módulos de JavaScript, las dependencias solo se ejecutan una vez en un contexto determinado (subproceso principal, trabajador, etc.), y todas las importaciones futuras hacen referencia a la instancia del módulo ya ejecutada. Los navegadores también optimizan la carga y la ejecución de los módulos de JavaScript. Las dependencias de un módulo se pueden cargar antes de que se ejecute el módulo, lo que permite que se carguen á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 usan en el subproceso principal y en un trabajador solo deben analizarse una vez.
El cambio a los módulos de JavaScript también permite el uso de la importación dinámica para el código de carga diferida sin bloquear la ejecución del trabajador. La importación dinámica es mucho más explícita que el uso de importScripts() para cargar dependencias,
ya que se devuelven las exportaciones del módulo importado en lugar de depender de variables globales.
worker.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 método importScripts() anterior no está disponible en los trabajadores de módulos. Cambiar los trabajadores para que usen módulos de JavaScript significa que todo el código se carga en modo estricto. Otro cambio notable es que el valor de this en el alcance de nivel superior de un módulo de JavaScript es undefined, mientras que en los trabajadores clásicos el valor es el alcance global del trabajador. Afortunadamente, siempre
ha habido un self global que proporciona una referencia al alcance global. Está disponible en
todos los tipos de trabajadores, incluidos los service workers, así como en el DOM.
Carga previamente los trabajadores con modulepreload
Una mejora de rendimiento sustancial que se incluye con los trabajadores de módulos es la capacidad de precargar
los trabajadores y sus dependencias. Con los trabajadores de módulos, las secuencias de comandos se cargan y ejecutan como módulos de JavaScript estándar, lo que significa que se pueden precargar y, incluso, analizar previamente con 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 trabajadores de módulos y el subproceso principal también pueden usar los módulos precargados. Esto es útil para los módulos que se importan en ambos contextos o en los casos en los que no es posible saber de antemano si se usará un módulo en el subproceso principal o en un trabajador.
Anteriormente, las opciones disponibles para precargar secuencias de comandos de trabajadores web eran limitadas y no
necesariamente confiables. Los trabajadores clásicos tenían su propio tipo de recurso "worker" para la carga previa, pero ningún navegador implementó <link rel="preload" as="worker">. Como resultado, la técnica principal
disponible para precargar trabajadores web era usar <link rel="prefetch">, que dependía por completo
de la caché HTTP. Cuando se usa en combinación con los encabezados de almacenamiento en caché correctos, esto permite evitar que la creación de instancias del trabajador tenga que esperar para descargar la secuencia de comandos del trabajador. Sin embargo, a diferencia de modulepreload, esta técnica no admitía la carga previa de dependencias ni el análisis previo.
¿Qué sucede con los trabajadores compartidos?
Los workers compartidos se actualizaron con compatibilidad con módulos de JavaScript a partir de Chrome 83. Al igual que con los workers dedicados,
la construcción de un worker compartido con la opción {type:"module"} ahora carga la secuencia de comandos del worker como un
módulo en lugar de una secuencia de comandos clásica:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
Antes de que se admitieran los módulos de JavaScript, el constructor SharedWorker() solo esperaba una URL y un argumento name opcional. Esto seguirá funcionando para el uso de trabajadores compartidos clásicos; 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 para un trabajador dedicado, incluida la opción name que reemplaza
el argumento name anterior.
¿Qué sucede con el service worker?
La especificación del service worker ya se actualizó para admitir la aceptación de un módulo de JavaScript como punto de entrada, con la misma opción {type:"module"} que los trabajadores de módulos. Sin embargo, este cambio aún no se implementó en los navegadores. Una vez que eso suceda, será posible
crear una instancia de un service worker con un módulo de JavaScript con el siguiente código:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
Ahora que se actualizó la especificación, los navegadores están comenzando a implementar el nuevo comportamiento. Esto lleva tiempo porque hay algunas complicaciones adicionales asociadas con la incorporación de módulos de JavaScript al service worker. El registro del service worker debe comparar las secuencias de comandos importadas con sus versiones almacenadas en caché anteriores cuando se determina si se debe activar una actualización, y esto debe implementarse para los módulos de JavaScript cuando se usan para service workers. Además, los service workers deben poder omitir la caché de las secuencias de comandos en ciertos casos cuando buscan actualizaciones.