Cómo ejecutar subprocesos en la Web con trabajadores del módulo

Con los módulos de JavaScript en los trabajadores web, ahora es más fácil mover el trabajo pesado a subprocesos en segundo plano.

JavaScript tiene un solo subproceso, lo que significa que solo puede realizar una operación a la vez. Este es intuitivo y funciona bien en muchos casos en la web, pero puede convertirse en un problema cuando necesitamos realizan tareas de mucho trabajo, como procesamiento, análisis, procesamiento o análisis de datos. Cada vez más y más complejas se entregan en la Web, hay una mayor necesidad de el procesamiento de datos.

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

Este es un ejemplo típico de uso de trabajador, en el que una secuencia de comandos de trabajador escucha los mensajes de la interfaz conversación y responde enviándole 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. Aunque significa que los trabajadores tienen una excelente asistencia para los navegadores y están bien optimizados, 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 el código en un trabajador y redactar secuencias de comandos sigue siendo similar a la secuencia de comandos síncrona de carga de datos es común en 2009.

Historial: trabajadores clásicos

El constructor de Worker toma una de secuencia de comandos, que es en relación con la URL del documento. Inmediatamente, devuelve una referencia a la nueva instancia de trabajador. que expone una interfaz de mensajería y un método terminate() que se detiene y destruirá al trabajador.

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

Hay una función importScripts() disponible en 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 en una secuencia de comandos se pueden reemplazadas por las variables de otro.

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';
}

Históricamente, los trabajadores web impusieron un efecto desmedido en la arquitectura de una y mantener la integridad de su aplicación. Los desarrolladores han tenido que crear herramientas y soluciones ingeniosas usar trabajadores web sin renunciar a las prácticas modernas de desarrollo. Por ejemplo, a los agrupadores les gusta webpack incorpora una implementación de cargador de módulos pequeño en el código generado que usa importScripts() para cargar código, pero une módulos en funciones para evitar colisiones de variables y simular y las exportaciones de dependencias.

Ingresar trabajadores del módulo

Un modo nuevo para los trabajadores web con los beneficios de ergonomía y rendimiento de JavaScript módulos se envían en Chrome 80, llamados trabajadores de módulos. El El constructor Worker ahora acepta una nueva opción {type:"module"}, que cambia la carga de la secuencia de comandos y ejecución para que coincida con <script type="module">.

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

Como los trabajadores del módulo son módulos estándar de JavaScript, pueden usar instrucciones de importación y exportación. Como 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. La carga y la ejecución de módulos de JavaScript también está optimizada por los navegadores. Las dependencias de un módulo pueden ser cargarse antes de la ejecución del módulo, lo que permite cargar árboles de módulos completos en en paralelo. La carga de módulos también almacena en caché el código analizado, es decir, los módulos que se usan en la instancia subproceso y en un trabajador solo deben analizarse una vez.

Pasar a los módulos de JavaScript también permite el uso de archivos import para la carga diferida de código sin bloquear la ejecución de el 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 el módulo. trabajadores. Cambiar los trabajadores para que usen módulos de JavaScript significa que todo el código se carga de forma estricta automático. Otro notable cambio es que el valor de this en el alcance de nivel superior de un módulo de JavaScript undefined, mientras que en los trabajadores clásicos, el valor es el permiso global del trabajador. Por suerte, no hay siempre fue un self global que proporciona una referencia al alcance global. Está disponible en para todos los tipos de trabajadores, incluidos los service workers, así como en el DOM.

Precargar los trabajadores con modulepreload

Una mejora sustancial del rendimiento que se incorpora a los trabajadores de módulos es la capacidad de precargar trabajadores y sus dependencias. Con los trabajadores de módulos, las secuencias de comandos se cargan y ejecutan de forma estándar. Módulos de JavaScript, lo que significa que se pueden cargar previamente o 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>

Tanto el subproceso principal como los trabajadores del módulo pueden usar los módulos precargados. Esto es útil para módulos que se importan en ambos contextos o en los casos en los que no es posible saber de antemano si un módulo se usará en el subproceso principal o en un worker.

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 "trabajador" el tipo de recurso para la precarga, pero no navegadores implementaron <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 en la caché HTTP. Cuando se utilizaba en combinación con los encabezados de almacenamiento en caché correctos, esto posibilitó para 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 precarga de dependencias ni el análisis previo.

¿Qué sucede con los trabajadores compartidos?

Los trabajadores compartidos tienen se actualizó y admitía módulos de JavaScript a partir de Chrome 83. Al igual que los trabajadores dedicados, crear un trabajador compartido con la opción {type:"module"} ahora carga la secuencia de comandos del trabajador como un 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() esperaba solo un elemento URL y un argumento opcional name. 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. Los recursos disponibles Opciones son los mismos que los de un trabajador dedicado, incluida la opción name que sustituye el argumento name anterior.

¿Qué ocurre con los service workers?

La especificación del service worker ya existe actualizado para admitir la aceptación de Módulo de JavaScript como punto de entrada, con la misma opción {type:"module"} que los trabajadores del módulo, Sin embargo, este cambio aún no se implementó en navegadores. Una vez que eso suceda, podrás para crear una instancia de un service worker usando 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 JavaScript módulos a un service worker. El registro de service worker debe comparar las secuencias de comandos importadas con sus versiones anteriores almacenadas en caché determinar si se debe activar una actualización (esto se debe implementar en los módulos de JavaScript) cuando se usan para service workers. Además, los service workers deben poder omitir las en caché para secuencias de comandos en ciertos casos buscar actualizaciones.

Recursos adicionales y lecturas adicionales