Long JavaScript calculations can pause typing, clicking, and painting when they run on the page's main thread. A dedicated Web Worker moves one page-owned job into another browser context so the page can send work to the background and render the returned message when it arrives.
The page talks to the worker through postMessage() and the message event. The worker script runs with its own global scope, so it cannot query or update the DOM directly; send plain data back and let the main thread change the interface.
Serve the files from an HTTP or HTTPS origin while testing. The worker script URL is resolved from the page, must be allowed by same-origin rules, and should be served as JavaScript; an error handler on the Worker object makes path, origin, or script-load failures visible.
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Web Worker demo</title> <script defer src="app.js"></script> </head> <body> <main id="worker-demo"> <h1>Web Worker calculation</h1> <p>The page sends four numbers to a dedicated worker.</p> <button id="run-worker" type="button">Run worker job</button> <p id="worker-result">No worker result yet.</p> <pre id="worker-log" aria-label="Worker message log"></pre> </main> </body> </html>
defer lets app.js run after the page elements exist.
Related: How to load JavaScript with defer
const runButton = document.querySelector("#run-worker"); const resultOutput = document.querySelector("#worker-result"); const logOutput = document.querySelector("#worker-log"); let activeWorker = null; const evidence = []; window.workerEvidence = evidence; window.lastWorkerResult = null; function writeLog(message) { evidence.push(message); logOutput.textContent = evidence.join("\n"); console.log(message); }
function createWorker() { if (!window.Worker) { resultOutput.textContent = "Web Workers are not supported in this browser."; return null; } const worker = new Worker("worker.js", { name: "sum-worker" }); writeLog("main: worker created"); worker.addEventListener("message", (event) => { const message = event.data; if (message.type === "progress") { writeLog(message.text); return; } if (message.type === "result") { window.lastWorkerResult = message; resultOutput.textContent = `Worker total: ${message.total}`; writeLog(`main: total ${message.total} from ${message.count} numbers`); worker.terminate(); if (activeWorker === worker) { activeWorker = null; } writeLog("main: worker terminated"); } }); worker.addEventListener("error", (event) => { resultOutput.textContent = `Worker failed: ${event.message}`; writeLog(`main: worker error at ${event.filename}:${event.lineno}`); if (activeWorker === worker) { activeWorker = null; } }); return worker; }
The Worker URL is relative to index.html in this plain-file setup. Bundled projects often use new Worker(new URL(“worker.js”, import.meta.url)) so the bundler can rewrite the worker file path.
runButton.addEventListener("click", () => { if (activeWorker) { activeWorker.terminate(); } evidence.length = 0; window.lastWorkerResult = null; logOutput.textContent = ""; resultOutput.textContent = "Waiting for worker..."; activeWorker = createWorker(); if (activeWorker) { activeWorker.postMessage({ numbers: [12, 31, 48, 5] }); writeLog("main: job posted"); } });
self.addEventListener("message", (event) => { const numbers = Array.isArray(event.data?.numbers) ? event.data.numbers.map(Number) : []; self.postMessage({ type: "progress", text: `worker: received ${numbers.length} numbers` }); const total = numbers.reduce((sum, number) => sum + number, 0); self.postMessage({ type: "result", total, count: numbers.length }); });
Use self inside the worker. Worker code has no direct document or window access, so app.js owns the visible page update.
$ python3 -m http.server 8000 --bind 127.0.0.1 Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
Do not open index.html directly from disk for this check. A local origin avoids browser-specific file: URL behavior and makes the worker script path test match a deployed page.
http://127.0.0.1:8000/
Worker total: 96 main: worker created main: job posted worker: received 4 numbers main: total 96 from 4 numbers main: worker terminated
The result line is written by app.js after it receives the worker's result message. The worker calculates the total but never updates the page directly.
^C