JavaScript
Web Workers y comunicación entre contextos
Explica cómo ejecutar trabajo fuera del hilo principal con Web Workers y cómo intercambiar mensajes y datos entre contextos de forma segura.
- Última actualización
- Actualizada
- Nivel
- Aplicación
JavaScript
Explica cómo ejecutar trabajo fuera del hilo principal con Web Workers y cómo intercambiar mensajes y datos entre contextos de forma segura.
Un Web Worker ejecuta JavaScript en otro contexto y agente, separado del hilo principal de la página.
const worker = new Worker(
new URL("./worker.js", import.meta.url),
{ type: "module" },
);El hilo principal suele encargarse de:
Un cálculo largo allí puede congelar la interfaz.
Pertenece al contexto que lo creó.
worker.postMessage({
type: "calculate",
values,
});En el worker:
self.addEventListener("message", (event) => {
const result = calculate(event.data.values);
self.postMessage({
type: "result",
result,
});
});Dentro del worker:
window; // no disponible
document; // no disponibleEl worker recibe datos, calcula y devuelve resultados. La página aplica cambios al DOM.
postMessage clona valores mediante structured clone.
Puede transferir:
No puede clonar funciones ni nodos DOM.
const buffer = new ArrayBuffer(1024);
worker.postMessage(buffer, [buffer]);La propiedad del almacenamiento se transfiere sin copiar todos los bytes. El buffer original queda detached.
buffer.byteLength; // normalmente 0 después de transferirNo sigas utilizándolo en el remitente.
const channel = new MessageChannel();
worker.postMessage(
{ type: "connect" },
[channel.port2],
);
channel.port1.postMessage(message);Crea dos puertos conectados y permite construir canales dedicados entre contextos.
worker.addEventListener("message", (event) => {
const message = parseWorkerMessage(event.data);
if (message.type === "result") {
render(message.result);
}
});Aunque provenga de tu propio worker, un contrato explícito evita estados imposibles y facilita versionado.
worker.addEventListener("error", (event) => {
reportWorkerError(event.error);
});
worker.addEventListener("messageerror", () => {
reportInvalidMessage();
});Un error no se propaga como excepción síncrona al código que llamó postMessage.
Desde la página:
worker.terminate();Desde el worker:
self.close();Terminar puede cancelar trabajo sin cleanup asíncrono completo. Diseña mensajes de cierre cuando necesitas guardar estado.
Puedes enviar un mensaje:
worker.postMessage({
type: "cancel",
jobId,
});El cálculo debe comprobar periódicamente si el job fue cancelado. terminate es una interrupción más brusca de todo el worker.
Costos:
Para tareas pequeñas, el overhead puede superar el beneficio.
Fetch ya es asíncrono y el navegador gestiona red fuera del stack. Mover una llamada fetch a un worker no la vuelve automáticamente más rápida.
Workers aportan sobre todo para:
Un SharedWorker puede ser compartido por varias ventanas del mismo origen y comunicarse mediante ports.
Su disponibilidad y uso práctico son más limitados; necesitas manejar múltiples conexiones y tiempo de vida independiente.
Permite memoria compartida entre agentes, normalmente bajo requisitos de cross-origin isolation.
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);La memoria compartida introduce data races. Atomics ofrece operaciones coordinadas.
No uses SharedArrayBuffer si el paso de mensajes resuelve el problema con un modelo más simple.
Atomics.add(view, 0, 1);
Atomics.load(view, 0);Ayuda a sincronizar acceso a memoria compartida. El diseño concurrente sigue siendo complejo: orden, espera, bloqueo y protocolos deben estar definidos.
Crear un worker por elemento puede ser costoso. Un pool reutiliza una cantidad limitada según trabajo y hardware.
jobs → queue → workers disponibles → resultsPágina:
const worker = new Worker(
new URL("./parse-worker.js", import.meta.url),
{ type: "module" },
);
worker.postMessage(file);
worker.addEventListener("message", (event) => {
const message = parseWorkerMessage(event.data);
renderPreview(message.rows);
});Worker:
self.addEventListener("message", async (event) => {
const text = await event.data.text();
const rows = parseLargeCsv(text);
self.postMessage({
type: "parsed",
rows,
});
});El parsing costoso no ocupa el hilo principal.
¿Por qué transferir un ArrayBuffer puede ser mejor que clonarlo?
Porque mueve la propiedad del mismo almacenamiento sin copiar todos sus bytes, reduciendo costo y memoria; a cambio, el remitente deja de poder usarlo.
Service workers y modo offline explica un worker con ciclo de vida orientado a red, cache y eventos del navegador.