Web Workers para la lógica del juego
Los Web Workers permiten ejecutar JavaScript en un hilo en segundo plano. En los juegos, esto significa que la física, la IA y la generación procedimental pueden ejecutarse sin provocar caídas de fotogramas.
1) Cuándo usar Workers
Buenos casos de uso:
- Simulación física
- Búsqueda de rutas (A*, navmesh)
- Toma de decisiones de la IA
- Generación procedimental
- Procesamiento de recursos (manipulación de imágenes, compresión)
- Matemáticas complejas (FFT, detección de colisiones)
No son ideales para:
- Renderizado (los Workers no pueden acceder directamente al DOM ni a Canvas)
- Tareas muy pequeñas (la sobrecarga del hilo no compensa)
2) Crear un Worker básico
worker.js:
self.onmessage = (e) => {
const { type, data } = e.data
if (type === 'calculate') {
const result = heavyCalculation(data)
self.postMessage({ type: 'result', data: result })
}
}
function heavyCalculation(input) {
// Trabajo intensivo aquí
return input * 2
}main.js:
const worker = new Worker('worker.js')
worker.onmessage = (e) => {
const { type, data } = e.data
if (type === 'result') {
console.log('Resultado obtenido:', data)
}
}
worker.postMessage({ type: 'calculate', data: 42 })3) Workers en línea (sin archivo separado)
function createInlineWorker(fn) {
const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
return new Worker(URL.createObjectURL(blob))
}
const worker = createInlineWorker(() => {
self.onmessage = (e) => {
const result = e.data * 2
self.postMessage(result)
}
})4) Workers como módulos ES
Puedes escribir el código del Worker como un módulo ES y usar import dentro de él, en lugar de importScripts. Pasa { type: 'module' } al constructor:
const worker = new Worker('physics-worker.js', { type: 'module' })Dentro del Worker puedes usar import para cargar utilidades compartidas de matemáticas, colisiones o IA del mismo modo que en el hilo principal, para no duplicar la lógica del juego. Los Workers como módulos son compatibles con Chrome y Edge 80+, Safari 15+ y Firefox 114+.
5) Ejemplo de Worker de física
physics-worker.js:
const bodies = []
const FIXED_DT = 1 / 60
self.onmessage = (e) => {
const { type, data } = e.data
switch (type) {
case 'init':
initWorld(data)
break
case 'step':
step()
break
case 'addBody':
bodies.push(data)
break
}
}
function initWorld(config) {
// Inicializar el mundo físico
}
function step() {
// Actualizar la física
for (const body of bodies) {
body.vy += 9.8 * FIXED_DT // Gravedad
body.x += body.vx * FIXED_DT
body.y += body.vy * FIXED_DT
}
// Devolver las posiciones
self.postMessage({
type: 'positions',
data: bodies.map(b => ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
})
}6) Objetos transferibles para mejorar el rendimiento
Los datos de gran tamaño pueden transferirse sin copiarlos:
// Hilo principal
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions ya no se puede usar aquí (se ha transferido)
// Worker
self.onmessage = (e) => {
const positions = e.data
// Trabajar con las posiciones
self.postMessage(positions, [positions.buffer])
}7) Worker para búsqueda de rutas
pathfinding-worker.js:
let grid = null
self.onmessage = (e) => {
const { type, data } = e.data
if (type === 'setGrid') {
grid = data
}
if (type === 'findPath') {
const path = aStar(data.start, data.end, grid)
self.postMessage({ type: 'path', id: data.id, path })
}
}
function aStar(start, end, grid) {
// Implementación de A*
const openSet = [start]
const cameFrom = new Map()
const gScore = new Map()
gScore.set(key(start), 0)
while (openSet.length > 0) {
// ... Lógica de A*
}
return reconstructPath(cameFrom, end)
}
function key(pos) {
return `${pos.x},${pos.y}`
}8) Grupo de Workers para tareas en paralelo
class WorkerPool {
constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
this.workers = []
this.queue = []
this.available = []
for (let i = 0; i < size; i++) {
const worker = new Worker(workerUrl)
worker.onmessage = (e) => this.handleResult(worker, e)
this.workers.push(worker)
this.available.push(worker)
}
}
run(data) {
return new Promise((resolve) => {
const task = { data, resolve }
if (this.available.length > 0) {
this.dispatch(this.available.pop(), task)
} else {
this.queue.push(task)
}
})
}
dispatch(worker, task) {
worker._currentTask = task
worker.postMessage(task.data)
}
handleResult(worker, e) {
const task = worker._currentTask
task.resolve(e.data)
if (this.queue.length > 0) {
this.dispatch(worker, this.queue.shift())
} else {
this.available.push(worker)
}
}
terminate() {
this.workers.forEach(w => w.terminate())
}
}9) SharedArrayBuffer para sincronización en tiempo real
Con el aislamiento entre orígenes activado, puedes compartir memoria:
// Hilo principal
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)
worker.postMessage({ type: 'init', buffer: shared })
// El Worker lee y escribe directamente en la memoria compartida
// Sin sobrecarga de postMessage para actualizar posiciones10) OffscreenCanvas para Workers
Renderiza en un Worker. OffscreenCanvas ya forma parte de Baseline y está ampliamente disponible en Chrome, Edge, Firefox y Safari (Safari añadió compatibilidad en la versión 17.0 para macOS e iOS):
// Hilo principal
const canvas = document.getElementById('game')
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: offscreen }, [offscreen])
// Worker
self.onmessage = (e) => {
const canvas = e.data.canvas
const ctx = canvas.getContext('2d')
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Dibujar...
requestAnimationFrame(render)
}
render()
}11) Gestión de errores
worker.onerror = (e) => {
console.error('Error del Worker:', e.message, e.filename, e.lineno)
}
// En el Worker
self.onerror = (e) => {
self.postMessage({ type: 'error', message: e.message })
}Contenido relacionado
- Publica un juego web que cargue rápido
- Activa los hilos de Wasm (SharedArrayBuffer)
- Bucle de juego con Canvas 2D
- Bibliotecas de física para juegos — ejecutar Rapier o Cannon-es en un Worker
- Carga de recursos por streaming — decodificar recursos fuera del hilo principal
- Stack tecnológico para juegos web en 2026 — cómo encajan los Workers en el stack de WebGL, WebGPU y Wasm
Recursos externos
- MDN: API de Web Workers — referencia completa de la API
- MDN: Uso de Web Workers — guía paso a paso
- MDN: Objetos transferibles — transferencia de datos sin copias
- MDN: SharedArrayBuffer — memoria compartida entre hilos