Ahora es más fácil mover las tareas pesadas a subprocesos en segundo plano con los módulos de JavaScript en los trabajadores web.
JavaScript tiene un solo subproceso, lo que significa que solo puede realizar una operación a la vez. Esto es intuitivo y funciona bien en muchos casos en la Web, pero puede ser problemático cuando necesitamos realizar tareas pesadas, como el procesamiento, el análisis, el procesamiento o el análisis de datos. A medida que se entregan cada vez más aplicaciones complejas en la Web, aumenta la necesidad de procesamiento en 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 de los subprocesos del sistema operativo que exponen una API que transmite mensajes para la comunicación entre subprocesos. Esto puede ser muy útil cuando se realizan cálculos costosos o se operan conjuntos de datos grandes, lo 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.
A continuación, se muestra un ejemplo típico del uso de trabajadores, en el que una secuencia de comandos de trabajador escucha 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 el navegador y están bien optimizados, también significa que son 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 compilar secuencias de comandos sigue siendo 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 está relacionada con 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 de inmediato al trabajador.
const worker = new Worker('worker.js');
Hay una función importScripts()
disponible en los trabajadores web para cargar código adicional, pero detiene la ejecución del trabajador para recuperar y evaluar cada secuencia de comandos. También ejecuta secuencias de comandos en el permiso global como una etiqueta <script>
clásica, lo que significa que las variables de una secuencia de comandos pueden reemplazarse 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 ingeniosas para permitir el uso de trabajadores web sin renunciar a las prácticas de desarrollo modernas. A modo de 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, llamado trabajadores de módulo. El constructor Worker
ahora acepta una nueva opción {type:"module"}
, que cambia la carga y ejecución de la secuencia de comandos 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 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 ejecución de los módulos de JavaScript. Las dependencias de un módulo se pueden cargar antes de que se ejecute, 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 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 la carga diferida de 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 se muestran 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 rendimiento excelente, el método importScripts()
anterior no está disponible en los trabajadores del módulo. Si cambias los trabajadores para que usen módulos de JavaScript, todo el código se cargará 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. Por suerte, 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 previa de trabajadores con modulepreload
Una mejora sustancial del rendimiento que se incluye con los trabajadores de módulo es la capacidad de precargar 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 hasta 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>
Tanto el subproceso principal como los trabajadores del módulo pueden usar los módulos precargados. Esto es útil para los módulos que se importan en ambos contextos o en los casos en 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 la carga previa de secuencias de comandos de trabajadores web eran limitadas y no necesariamente confiables. Los trabajadores clásicos tenían su propio tipo de recurso "trabajador" para la carga previa, pero ningún navegador implementó <link rel="preload" as="worker">
. Como resultado, la técnica principal disponible para la carga previa de trabajadores web era usar <link rel="prefetch">
, que se basaba por completo en 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 de 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 trabajadores compartidos se actualizaron con compatibilidad para módulos de 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 la secuencia de comandos del trabajador como un módulo en lugar de una secuencia de comandos clásica:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
Antes de admitir los módulos de JavaScript, el constructor SharedWorker()
solo esperaba una URL y un argumento name
opcional. Esto seguirá funcionando para el uso clásico de trabajadores compartidos. Sin embargo, para crear trabajadores compartidos de módulos, se 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 el argumento name
anterior.
¿Qué ocurre con los service workers?
La especificación del trabajador de servicio ya se actualizó para admitir la aceptación de un módulo JavaScript como punto de entrada, con la misma opción {type:"module"}
que los trabajadores de módulo. Sin embargo, este cambio aún no se implementó en los navegadores. Una vez que eso suceda, será posible crear una instancia de un trabajador de servicio 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 trabajador de servicio. El registro de service workers debe comparar las secuencias de comandos importadas con sus versiones anteriores almacenadas en caché para determinar si se debe activar una actualización. Esto se debe implementar en los módulos de JavaScript cuando se usan con service workers. Además, los trabajadores del servicio deben poder omitir la caché de las secuencias de comandos en ciertos casos cuando se verifican las actualizaciones.