Performancemedium

Offload heavy parsing to a Web Worker (with cancellation)

You parse large JSON / log blobs in the browser and the main thread janks. Wrap a Web Worker in a clean Promise-based API with cancellation and request IDs.

Asked at CrowdStrike, Google

#web workers#performance#concurrency#cancellation

Answer

// worker.js — runs off the main thread
self.onmessage = (e) => {
  const { id, payload } = e.data
  try {
    // Replace with real heavy work (parse, decompress, indexing, etc.)
    const result = JSON.parse(payload)
    self.postMessage({ id, ok: true, result })
  } catch (err) {
    self.postMessage({ id, ok: false, error: String(err) })
  }
}

// workerClient.js — main thread API
export function createWorkerClient(workerUrl) {
  const worker = new Worker(workerUrl, { type: 'module' })
  const pending = new Map() // id -> { resolve, reject, signal, onAbort }
  let nextId = 0

  worker.onmessage = (e) => {
    const { id, ok, result, error } = e.data
    const entry = pending.get(id)
    if (!entry) return // already cancelled
    pending.delete(id)
    entry.signal?.removeEventListener('abort', entry.onAbort)
    ok ? entry.resolve(result) : entry.reject(new Error(error))
  }

  function run(payload, { signal } = {}) {
    if (signal?.aborted) return Promise.reject(new DOMException('Aborted', 'AbortError'))

    return new Promise((resolve, reject) => {
      const id = nextId++
      const onAbort = () => {
        if (!pending.has(id)) return
        pending.delete(id)
        reject(new DOMException('Aborted', 'AbortError'))
        // Cooperative cancel: tell the worker to drop this id if it supports it.
        worker.postMessage({ id, cancel: true })
      }
      pending.set(id, { resolve, reject, signal, onAbort })
      signal?.addEventListener('abort', onAbort, { once: true })
      worker.postMessage({ id, payload })
    })
  }

  function terminate() {
    worker.terminate()
    pending.forEach((p) => p.reject(new Error('worker terminated')))
    pending.clear()
  }

  return { run, terminate }
}

// Usage
const client = createWorkerClient(new URL('./worker.js', import.meta.url))
const ctrl = new AbortController()
client.run(bigJsonString, { signal: ctrl.signal })
  .then((parsed) => render(parsed))
  .catch((err) => { if (err.name !== 'AbortError') reportError(err) })

// Cancel if the user navigates away:
ctrl.abort()

// Interview talking points:
// - Use Transferable objects (ArrayBuffer) for zero-copy where possible.
// - Pool workers when calls are frequent (creation cost is ~1-5ms).
// - Workers don't have DOM access; serialize data, not functions.

Practise more Performance questions →