Skip to content

Web Workers untuk logika game ​

Web Workers memungkinkan Anda menjalankan JavaScript di thread latar belakang. Untuk game, ini berarti fisika, AI, dan generasi prosedural dapat berjalan tanpa menyebabkan penurunan frame rate.

1) Kapan harus menggunakan Workers ​

Cocok digunakan untuk:

  • Simulasi fisika
  • Pencarian jalur (A*, navmesh)
  • Pengambilan keputusan AI
  • Generasi prosedural
  • Pemrosesan aset (manipulasi gambar, kompresi)
  • Matematika kompleks (FFT, deteksi tabrakan)

Kurang ideal untuk:

  • Rendering (Workers tidak dapat mengakses DOM/Canvas secara langsung)
  • Tugas yang sangat kecil (overhead thread tidak sepadan)

2) Membuat Worker dasar ​

worker.js:

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) {
  // Expensive work here
  return input * 2
}

main.js:

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

worker.onmessage = (e) => {
  const { type, data } = e.data
  if (type === 'result') {
    console.log('Got result:', data)
  }
}

worker.postMessage({ type: 'calculate', data: 42 })

3) Worker inline (tanpa berkas terpisah) ​

js
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) Worker modul ES ​

Anda dapat menulis kode worker sebagai modul ES dan menggunakan import di dalamnya sebagai pengganti importScripts. Teruskan { type: 'module' } ke konstruktor:

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

Di dalam worker, Anda kemudian dapat menggunakan import untuk helper matematika, tabrakan, atau AI bersama dengan cara yang sama seperti di thread utama, sehingga logika game tidak perlu diduplikasi. Worker modul didukung di Chrome dan Edge 80+, Safari 15+, serta Firefox 114+.

5) Contoh Worker fisika ​

physics-worker.js:

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) {
  // Initialize physics world
}

function step() {
  // Update physics
  for (const body of bodies) {
    body.vy += 9.8 * FIXED_DT // Gravity
    body.x += body.vx * FIXED_DT
    body.y += body.vy * FIXED_DT
  }
  
  // Send positions back
  self.postMessage({
    type: 'positions',
    data: bodies.map(b => ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
  })
}

6) Objek transferable untuk performa ​

Data berukuran besar dapat ditransfer tanpa disalin:

js
// Main thread
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions is now unusable here (transferred)

// Worker
self.onmessage = (e) => {
  const positions = e.data
  // Work with positions
  self.postMessage(positions, [positions.buffer])
}

7) Worker pencarian jalur ​

pathfinding-worker.js:

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) {
  // A* implementation
  const openSet = [start]
  const cameFrom = new Map()
  const gScore = new Map()
  gScore.set(key(start), 0)
  
  while (openSet.length > 0) {
    // ... A* logic
  }
  
  return reconstructPath(cameFrom, end)
}

function key(pos) {
  return `${pos.x},${pos.y}`
}

8) Pool Worker untuk tugas paralel ​

js
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 untuk sinkronisasi real-time ​

Dengan isolasi lintas origin yang diaktifkan, Anda dapat berbagi memori:

js
// Main thread
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)

worker.postMessage({ type: 'init', buffer: shared })

// Worker reads/writes directly to shared memory
// No postMessage overhead for position updates

10) OffscreenCanvas untuk Workers ​

Lakukan rendering di dalam Worker. OffscreenCanvas kini tersedia secara luas sebagai Baseline di Chrome, Edge, Firefox, dan Safari (Safari menambahkan dukungan pada versi 17.0 di macOS dan iOS):

js
// Main thread
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)
    // Draw...
    requestAnimationFrame(render)
  }
  render()
}

11) Penanganan error ​

js
worker.onerror = (e) => {
  console.error('Worker error:', e.message, e.filename, e.lineno)
}

// In worker
self.onerror = (e) => {
  self.postMessage({ type: 'error', message: e.message })
}

Terkait ​

Sumber Daya Eksternal ​