[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-weekly-web-workers":44,"search-suggestions":60,"quiz-article-weekly-web-workers":106},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a018ca-683e-71ec-b6a5-7b3e09d8aca9","weekly-web-workers","PRACTICE_QUIZ","Web Workers in JavaScript","Test what stuck about the main-thread freeze problem, message passing, transferable objects, error handling, and how workers differ from Service Workers.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,68,72,76,80,84,88,92,96,99,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",84,{"slug":66,"name":67,"articles":53},"javascript","Javascript",{"slug":69,"name":70,"articles":71},"frontend","Frontend",68,{"slug":73,"name":74,"articles":75},"css","Css",30,{"slug":77,"name":78,"articles":79},"tutorial","Tutorial",18,{"slug":81,"name":82,"articles":83},"performance","Performance",12,{"slug":85,"name":86,"articles":87},"typescript","Typescript",11,{"slug":89,"name":90,"articles":91},"react","React",9,{"slug":93,"name":94,"articles":95},"browser","Browser",6,{"slug":97,"name":98,"articles":95},"grammar","Grammar",{"slug":100,"name":101,"articles":102},"html","Html",5,{"slug":104,"name":105,"articles":102},"node","Node",{"id":107,"slug":46,"title":108,"subtitle":52,"excerpt":109,"coverUrl":110,"locale":13,"readingMinutes":83,"publishedAt":111,"viewCount":112,"likeCount":19,"commentCount":19,"author":113,"vertical":118,"topic":119,"tags":121,"_count":126,"playground":128,"body":130,"bodyMd":470,"seo":471,"translationGroupId":473,"series":474,"podcastUrl":52,"verticalId":5,"thread":506,"assessments":508,"translations":511,"quiz":513},"01a018ca-67d5-7020-bce5-9816c38a7a54","Web Workers in JavaScript: The Complete Guide","Learn JavaScript Web Workers step by step: message passing, transferable objects, error handling, and a copy-paste cheat sheet for offloading work.","\u002Fmedia\u002Fcovers\u002Fweekly-web-workers.png","2026-09-07T12:03:05.323Z",38,{"id":114,"name":115,"username":116,"avatarUrl":52,"headline":117},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":120},"JavaScript",[122,123,124,125],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":77,"name":78,"color":52},{"slug":81,"name":82,"color":52},{"assessments":127},1,{"slug":46,"title":129},"Main thread vs. Web Worker — interactive playground",{"blocks":131,"version":127},[132,136,141,144,153,156,159,170,173,176,182,185,188,191,194,197,200,203,206,209,213,217,221,224,227,230,234,237,240,243,247,251,254,257,260,264,267,270,273,276,280,283,286,296,299,302,305,308,312,315,318,321,324,327,330,334,337,341,344,348,351,354,357,360,407,411,414,417,420,423,426,429,437,440,443,446,449,452,455,458,461,464],{"id":133,"html":134,"type":135},"b1","\u003Cp>Drag a slider on a page that&#39;s mid-way through parsing a 50,000-row CSV in JavaScript, and nothing happens for a full second. Not because the slider&#39;s code is slow — it never even runs. The browser&#39;s one JavaScript thread is busy with your parsing loop, and until that loop returns, no click, no scroll, no repaint gets a turn. The fix already ships in every browser you support: a second thread, called a Web Worker, that runs your code without ever touching the one thread the page&#39;s UI depends on.\u003C\u002Fp>","paragraph",{"id":137,"html":138,"text":139,"type":140,"level":31},"b2","What you&#39;ll learn","What you'll learn","heading",{"id":142,"html":143,"type":135},"b3","\u003Cp>By the end of this guide you&#39;ll be able to:\u003C\u002Fp>",{"id":145,"type":146,"items":147,"ordered":18},"b4","list",[148,149,150,151,152],"Explain why the browser has exactly one thread for JavaScript and the DOM, and why that thread stalls the whole page under heavy work","Create a Web Worker, send it data, and get a result back without blocking the main thread","Reason correctly about what does and doesn&#39;t survive the trip between threads (structured cloning vs. transferable objects)","Handle worker errors, terminate workers cleanly, and avoid the memory leaks that come from forgetting to","Decide, with real judgment, when a worker is worth the complexity and when it isn&#39;t",{"id":154,"html":155,"type":135},"b5","\u003Cp>\u003Cstrong>Who this is for:\u003C\u002Fstrong> you&#39;ve written \u003Ccode>addEventListener\u003C\u002Fcode> handlers and used \u003Ccode>fetch\u003C\u002Fcode>, and you&#39;ve felt a UI stutter you couldn&#39;t explain.\u003C\u002Fp>",{"id":157,"html":158,"text":158,"type":140,"level":31},"b6","Contents",{"id":160,"type":146,"items":161,"ordered":18},"b7",[162,163,164,165,166,167,168,169],"\u003Ca href=\"#why-web-workers-exist\">Why Web Workers exist\u003C\u002Fa>","\u003Ca href=\"#the-mental-model\">The mental model\u003C\u002Fa>","\u003Ca href=\"#building-a-worker-stage-by-stage\">Building a worker, stage by stage\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":171,"html":172,"text":172,"type":140,"level":31},"b8","Why Web Workers exist",{"id":174,"html":175,"type":135},"b9","\u003Cp>JavaScript in the browser runs on a single thread — the \u003Cstrong>main thread\u003C\u002Fstrong> — and that same thread is also responsible for parsing HTML, computing styles, laying out and painting pixels, and responding to input. When your code runs, everything else waits. Here&#39;s the naïve version of the CSV problem from the opening line:\u003C\u002Fp>",{"id":177,"code":178,"type":179,"language":180,"highlight":181},"b10","\u002F\u002F the wrong way — this blocks everything else on the page\nfunction sumColumn(rows, columnIndex) {\n  let total = 0;\n  for (let i = 0; i \u003C rows.length; i++) {\n    total += Number(rows[i][columnIndex]);\n  }\n  return total; \u002F\u002F for 50,000+ rows, this can easily take 500ms–2s\n}\n\nbutton.addEventListener(\"click\", () => {\n  const total = sumColumn(hugeDataset, 3); \u002F\u002F main thread stalls here\n  resultEl.textContent = total;\n});","code","js",[],{"id":183,"html":184,"type":135},"b11","\u003Cp>While \u003Ccode>sumColumn\u003C\u002Fcode> runs, the browser cannot repaint, cannot fire scroll or click handlers, and cannot update anything on screen — including a &quot;loading&quot; spinner you might have shown a moment earlier. The tab looks frozen because, for that stretch of time, it is. \u003Ccode>setTimeout(fn, 0)\u003C\u002Fcode> doesn&#39;t help either: it still runs the callback on the same main thread, just slightly later; it defers the freeze, it doesn&#39;t remove it.\u003C\u002Fp>",{"id":186,"html":187,"type":135},"b12","\u003Cp>A Web Worker solves this by giving that expensive loop its own thread, with its own JavaScript engine instance, running in parallel with the main thread. The main thread stays free to paint and respond to input the entire time.\u003C\u002Fp>",{"id":189,"html":190,"text":190,"type":140,"level":31},"b13","The mental model",{"id":192,"html":193,"type":135},"b14","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a Web Worker is a separate JavaScript environment, running in parallel, that shares no memory with the page — the only way in or out is sending copies of data through a message channel.\u003C\u002Fp>",{"id":195,"html":196,"type":135},"b15","\u003Cp>Picture two rooms with no shared furniture and no window between them, connected by a mail slot. You can pass a note through the slot (\u003Ccode>postMessage\u003C\u002Fcode>), and the other room can read it and mail one back (\u003Ccode>onmessage\u003C\u002Fcode>). Neither room can reach into the other and grab a variable, call a function, or touch a DOM element sitting in the other room — because nothing is actually shared. What crosses the slot is a \u003Cstrong>copy\u003C\u002Fstrong> of the data, produced by an algorithm called \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FStructured_clone_algorithm\">structured cloning\u003C\u002Fa>, not a reference to the original.\u003C\u002Fp>",{"id":198,"html":199,"type":135},"b16","\u003Cp>That single fact — no shared memory, only copied messages — explains almost every rule that follows: why a worker can&#39;t touch the DOM (the DOM objects live in the main thread&#39;s room), why you can&#39;t pass a function to a worker (functions aren&#39;t cloneable), and why very large payloads need a different trick (transferable objects, covered below).\u003C\u002Fp>",{"id":201,"html":202,"text":202,"type":140,"level":31},"b17","Building a worker, stage by stage",{"id":204,"html":205,"text":205,"type":140,"level":43},"b18","Stage 1: the smallest working worker",{"id":207,"html":208,"type":135},"b19","\u003Cp>A worker is created from a separate script file:\u003C\u002Fp>",{"id":210,"code":211,"type":179,"language":180,"highlight":212},"b20","\u002F\u002F main.js\nconst worker = new Worker(\"sum-worker.js\");\n\nworker.postMessage({ rows: hugeDataset, columnIndex: 3 }); \u002F\u002F send a copy of the data\n\nworker.onmessage = (event) => {\n  resultEl.textContent = event.data.total; \u002F\u002F runs once the worker replies\n};",[],{"id":214,"code":215,"type":179,"language":180,"highlight":216},"b21","\u002F\u002F sum-worker.js — runs on its own thread\nself.onmessage = (event) => {\n  const { rows, columnIndex } = event.data;\n  let total = 0;\n  for (let i = 0; i \u003C rows.length; i++) {\n    total += Number(rows[i][columnIndex]);\n  }\n  self.postMessage({ total }); \u002F\u002F send the result back as a message\n};",[],{"id":218,"html":219,"type":220},"b22","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the worker file has no access to \u003Ccode>window\u003C\u002Fcode> or the DOM — inside it, \u003Ccode>self\u003C\u002Fcode> refers to the worker&#39;s own global scope (\u003Ccode>DedicatedWorkerGlobalScope\u003C\u002Fcode>), not the page.\u003C\u002Fp>\n","quote",{"id":222,"html":223,"type":135},"b23","\u003Cp>Clicking the button now posts the data to the worker and returns immediately; the main thread never blocks. The heavy loop runs on the worker&#39;s thread, and the page keeps painting and responding to input the whole time.\u003C\u002Fp>",{"id":225,"html":226,"text":226,"type":140,"level":43},"b24","Stage 2: loading a worker without a separate file",{"id":228,"html":229,"type":135},"b25","\u003Cp>You don&#39;t always want a second network request for a small worker script. You can build one from a string using a \u003Ccode>Blob\u003C\u002Fcode> and an object URL:\u003C\u002Fp>",{"id":231,"code":232,"type":179,"language":180,"highlight":233},"b26","const workerSource = `\n  self.onmessage = (event) => {\n    const n = event.data;\n    self.postMessage(fib(n));\n  };\n  function fib(n) { return n \u003C 2 ? n : fib(n - 1) + fib(n - 2); }\n`;\n\nconst blob = new Blob([workerSource], { type: \"application\u002Fjavascript\" });\nconst worker = new Worker(URL.createObjectURL(blob));",[],{"id":235,"html":236,"type":135},"b27","\u003Cp>This is how the playground below builds its worker inline — useful for demos, small utility workers, or libraries that want to ship a worker without a second file to deploy.\u003C\u002Fp>",{"id":238,"html":239,"text":239,"type":140,"level":43},"b28","Stage 3: module workers",{"id":241,"html":242,"type":135},"b29","\u003Cp>Classic workers load dependencies with the older \u003Ccode>importScripts()\u003C\u002Fcode> function. Modern browsers also support \u003Cstrong>module workers\u003C\u002Fstrong>, which use standard \u003Ccode>import\u003C\u002Fcode> statements, by passing \u003Ccode>{ type: &quot;module&quot; }\u003C\u002Fcode>:\u003C\u002Fp>",{"id":244,"code":245,"type":179,"language":180,"highlight":246},"b30","const worker = new Worker(\"sum-worker.js\", { type: \"module\" });",[],{"id":248,"code":249,"type":179,"language":180,"highlight":250},"b31","\u002F\u002F sum-worker.js as a module\nimport { sumColumn } from \".\u002Fmath-utils.js\";\n\nself.onmessage = (event) => {\n  self.postMessage(sumColumn(event.data.rows, event.data.columnIndex));\n};",[],{"id":252,"html":253,"type":135},"b32","\u003Cp>Module workers are supported in all current major browsers as of 2026. If you need to support an environment that predates module worker support, stick with a classic worker and \u003Ccode>importScripts()\u003C\u002Fcode>.\u003C\u002Fp>",{"id":255,"html":256,"text":256,"type":140,"level":43},"b33","Stage 4: transferable objects, for when copying is too slow",{"id":258,"html":259,"type":135},"b34","\u003Cp>Structured cloning copies data. For a plain object with a few numbers, that copy is instant. For a 200 MB \u003Ccode>ArrayBuffer\u003C\u002Fcode> of audio or image data, copying it on every message becomes the new bottleneck. The fix is a \u003Cstrong>transferable object\u003C\u002Fstrong>: instead of copying an \u003Ccode>ArrayBuffer\u003C\u002Fcode>, you \u003Cem>transfer ownership\u003C\u002Fem> of it to the worker.\u003C\u002Fp>",{"id":261,"code":262,"type":179,"language":180,"highlight":263},"b35","const buffer = new ArrayBuffer(1024 * 1024 * 50); \u002F\u002F 50 MB\nworker.postMessage(buffer, [buffer]); \u002F\u002F second argument: the transfer list\n\n\u002F\u002F after this call, `buffer.byteLength` on the main thread is 0 —\n\u002F\u002F the memory now belongs to the worker, not this thread",[],{"id":265,"html":266,"type":220},"b36","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> transferring moves the underlying memory instead of copying it, which is why it&#39;s effectively free even for huge buffers — and why the original reference becomes unusable afterward.\u003C\u002Fp>\n",{"id":268,"html":269,"type":135},"b37","\u003Cp>\u003Ccode>ArrayBuffer\u003C\u002Fcode>, \u003Ccode>MessagePort\u003C\u002Fcode>, \u003Ccode>ImageBitmap\u003C\u002Fcode>, and a few stream types are transferable; plain objects, arrays, and strings are not — they&#39;re always copied.\u003C\u002Fp>",{"id":271,"html":272,"text":272,"type":140,"level":43},"b38","Stage 5: cleanup and termination",{"id":274,"html":275,"type":135},"b39","\u003Cp>A worker keeps running (and keeps memory allocated) until you explicitly stop it:\u003C\u002Fp>",{"id":277,"code":278,"type":179,"language":180,"highlight":279},"b40","\u002F\u002F from the main thread\nworker.terminate(); \u002F\u002F stops the worker immediately, no matter what it's doing\n\n\u002F\u002F from inside the worker itself\nself.close(); \u002F\u002F the worker asks to stop itself, after finishing current work",[],{"id":281,"html":282,"type":135},"b41","\u003Cp>\u003Ccode>terminate()\u003C\u002Fcode> is immediate and unconditional — any in-progress work in the worker is simply discarded, with no \u003Ccode>finally\u003C\u002Fcode> block guaranteed to run. Always terminate a worker you no longer need (e.g., when a component unmounts), or it keeps running and holding memory for the lifetime of the page.\u003C\u002Fp>",{"id":284,"html":285,"text":285,"type":140,"level":31},"b42","Edge cases and gotchas",{"id":287,"type":146,"items":288,"ordered":18},"b43",[289,290,291,292,293,294,295],"\u003Cstrong>No DOM access, ever.\u003C\u002Fstrong> A worker cannot read or write \u003Ccode>document\u003C\u002Fcode>, cannot use \u003Ccode>window\u003C\u002Fcode>, and cannot manipulate any DOM node you pass it — attempting to send a DOM node throws a \u003Ccode>DataCloneError\u003C\u002Fcode>, because DOM nodes are not structured-cloneable. If a worker needs to render something, it computes data and sends it back for the main thread to draw (or uses \u003Ccode>OffscreenCanvas\u003C\u002Fcode>, a separate, more advanced API).","\u003Cstrong>Functions can&#39;t cross the boundary.\u003C\u002Fstrong> You can&#39;t \u003Ccode>postMessage\u003C\u002Fcode> a callback and have the worker invoke it. Send data in, get data out; the worker&#39;s own script defines what it does with the data.","\u003Cstrong>Errors don&#39;t throw where you&#39;d expect.\u003C\u002Fstrong> An uncaught exception inside a worker doesn&#39;t throw on the main thread — it fires an \u003Ccode>error\u003C\u002Fcode> event on the \u003Ccode>Worker\u003C\u002Fcode> object. Always attach a handler:\n\n\u003Ccode>js worker.onerror = (event) =&gt; {   console.error(&quot;Worker crashed:&quot;, event.message, event.filename, event.lineno); }; \u003C\u002Fcode>\n\nWithout this handler, a worker that throws simply goes silent from the main thread&#39;s point of view.","\u003Cstrong>Same-origin restriction.\u003C\u002Fstrong> A classic \u003Ccode>new Worker(url)\u003C\u002Fcode> script must be same-origin with the page (a \u003Ccode>blob:\u003C\u002Fcode> URL created by the page counts as same-origin for this purpose). You cannot point a worker directly at a third-party script URL.","\u003Cstrong>Workers aren&#39;t free to start.\u003C\u002Fstrong> Spinning one up has real overhead — allocating a thread, a new JS engine context, and loading the script. For a task that finishes in a few milliseconds, that overhead can cost more than the task itself. Workers pay off for work that&#39;s substantial or repeated, not for trivial one-off computations.","\u003Cstrong>A worker can spawn its own workers\u003C\u002Fstrong>, and can also use \u003Ccode>fetch\u003C\u002Fcode>, \u003Ccode>WebSocket\u003C\u002Fcode>, \u003Ccode>setTimeout\u003C\u002Fcode>, and \u003Ccode>IndexedDB\u003C\u002Fcode> — it&#39;s a real JavaScript environment, just without the DOM.","\u003Cstrong>\u003Ccode>SharedWorker\u003C\u002Fcode> is a different, less common API.\u003C\u002Fstrong> It allows multiple tabs from the same origin to connect to one shared worker instance via \u003Ccode>port.postMessage\u003C\u002Fcode>, instead of each tab getting its own dedicated worker. Check current browser support before relying on it — support has historically lagged \u003Ccode>Worker\u003C\u002Fcode> and \u003Ccode>type: &quot;module&quot;\u003C\u002Fcode>.",{"id":297,"html":298,"text":298,"type":140,"level":31},"b44","Best practices",{"id":300,"html":301,"type":135},"b45","\u003Cp>\u003Cstrong>Reach for a worker when\u003C\u002Fstrong> you have CPU-bound work that takes tens of milliseconds or more — parsing large files, running compression, image or audio processing, complex data transformations, cryptography, or search\u002Ffiltering over large in-memory datasets — especially if it needs to run while the user keeps interacting with the page.\u003C\u002Fp>",{"id":303,"html":304,"type":135},"b46","\u003Cp>\u003Cstrong>Avoid it when\u003C\u002Fstrong> the work is already I\u002FO-bound (a \u003Ccode>fetch\u003C\u002Fcode> call doesn&#39;t block the main thread even without a worker — the wait for the network happens off-thread already), when the data involved is small enough that structured cloning would cost more than the computation itself, or when the task is short and infrequent enough that a worker&#39;s startup cost dominates.\u003C\u002Fp>",{"id":306,"html":307,"type":135},"b47","\u003Cp>\u003Cstrong>Keep the message contract simple.\u003C\u002Fstrong> Design worker messages like a small API: a \u003Ccode>type\u003C\u002Fcode> field plus a payload, both directions. This scales cleanly to a worker that handles more than one kind of task.\u003C\u002Fp>",{"id":309,"code":310,"type":179,"language":180,"highlight":311},"b48","worker.postMessage({ type: \"PARSE_CSV\", payload: csvText });\nworker.postMessage({ type: \"SORT_ROWS\", payload: { rows, key: \"date\" } });",[],{"id":313,"html":314,"type":135},"b49","\u003Cp>\u003Cstrong>Always pair creation with cleanup.\u003C\u002Fstrong> Every \u003Ccode>new Worker(...)\u003C\u002Fcode> should have a matching \u003Ccode>terminate()\u003C\u002Fcode> — in a component&#39;s unmount\u002Fcleanup hook, in a &quot;cancel&quot; button, or when the task naturally completes and you don&#39;t plan to reuse the worker.\u003C\u002Fp>",{"id":316,"html":317,"text":317,"type":140,"level":31},"b50","FAQ",{"id":319,"html":320,"text":320,"type":140,"level":43},"b51","Can a Web Worker access the DOM?",{"id":322,"html":323,"type":135},"b52","\u003Cp>No. Workers run in a context with no DOM APIs at all — no \u003Ccode>document\u003C\u002Fcode>, no \u003Ccode>window\u003C\u002Fcode>. If a worker needs something rendered, it sends data back to the main thread, which does the actual DOM update.\u003C\u002Fp>",{"id":325,"html":326,"text":326,"type":140,"level":43},"b53","Do Web Workers block the main thread while they run?",{"id":328,"html":329,"type":135},"b54","\u003Cp>No — that&#39;s their entire purpose. The worker&#39;s code executes on its own thread, in parallel with the main thread, so the page keeps painting and responding to input while the worker is busy.\u003C\u002Fp>",{"id":331,"html":332,"text":333,"type":140,"level":43},"b55","Can I use \u003Ccode>fetch\u003C\u002Fcode> or \u003Ccode>WebSocket\u003C\u002Fcode> inside a worker?","Can I use fetch or WebSocket inside a worker?",{"id":335,"html":336,"type":135},"b56","\u003Cp>Yes. Workers have access to \u003Ccode>fetch\u003C\u002Fcode>, \u003Ccode>WebSocket\u003C\u002Fcode>, \u003Ccode>setTimeout\u003C\u002Fcode>\u002F\u003Ccode>setInterval\u003C\u002Fcode>, \u003Ccode>IndexedDB\u003C\u002Fcode>, and \u003Ccode>self.crypto\u003C\u002Fcode>, among other APIs. What they lack is anything DOM-related.\u003C\u002Fp>",{"id":338,"html":339,"text":340,"type":140,"level":43},"b57","What&#39;s the difference between a Web Worker and a Service Worker?","What's the difference between a Web Worker and a Service Worker?",{"id":342,"html":343,"type":135},"b58","\u003Cp>A \u003Ccode>Worker\u003C\u002Fcode> is created and owned by one page for offloading computation and dies with that page (unless you use \u003Ccode>SharedWorker\u003C\u002Fcode>, which multiple tabs can connect to). A \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FService_Worker_API\">\u003Ccode>ServiceWorker\u003C\u002Fcode>\u003C\u002Fa> is a different API entirely: it&#39;s registered for an origin, keeps running independently of any open tab, and exists mainly to intercept network requests and enable offline support and push notifications. They solve different problems and aren&#39;t interchangeable.\u003C\u002Fp>",{"id":345,"html":346,"text":347,"type":140,"level":43},"b59","Does \u003Ccode>postMessage\u003C\u002Fcode> copy my data or send a reference to it?","Does postMessage copy my data or send a reference to it?",{"id":349,"html":350,"type":135},"b60","\u003Cp>By default it copies, using the structured clone algorithm — the receiving side gets an independent copy, and mutating one side afterward never affects the other. The exception is objects you explicitly list in the transfer list (like \u003Ccode>ArrayBuffer\u003C\u002Fcode>), which move instead of copying.\u003C\u002Fp>",{"id":352,"html":353,"text":353,"type":140,"level":43},"b61","Can I use React, Vue, or other framework code inside a worker?",{"id":355,"html":356,"type":135},"b62","\u003Cp>You can run plain JavaScript logic (data transforms, computation, parsing) inside a worker just fine, but you cannot render framework components there, because rendering ultimately means touching the DOM, and workers have no DOM access. Keep workers for the computation; keep rendering on the main thread.\u003C\u002Fp>",{"id":358,"html":359,"text":359,"type":140,"level":31},"b63","Cheat sheet",{"id":361,"head":362,"rows":366,"type":406},"b64",[363,364,365],"Task","Code","Notes",[367,371,375,379,383,387,391,394,398,402],[368,369,370],"Create a worker","\u003Ccode>new Worker(&quot;file.js&quot;)\u003C\u002Fcode>","Script must be same-origin (or a \u003Ccode>blob:\u003C\u002Fcode> URL)",[372,373,374],"Create a module worker","\u003Ccode>new Worker(&quot;file.js&quot;, { type: &quot;module&quot; })\u003C\u002Fcode>","Lets the worker use \u003Ccode>import\u003C\u002Fcode>",[376,377,378],"Send data to a worker","\u003Ccode>worker.postMessage(data)\u003C\u002Fcode>","Copies \u003Ccode>data\u003C\u002Fcode> via structured clone",[380,381,382],"Transfer instead of copy","\u003Ccode>worker.postMessage(buf, [buf])\u003C\u002Fcode>","Only for transferable types (e.g. \u003Ccode>ArrayBuffer\u003C\u002Fcode>); \u003Ccode>buf\u003C\u002Fcode> becomes unusable on the sender&#39;s side afterward",[384,385,386],"Receive data (main thread)","\u003Ccode>worker.onmessage = (e) =&gt; e.data\u003C\u002Fcode>","",[388,389,390],"Receive data (inside worker)","\u003Ccode>self.onmessage = (e) =&gt; e.data\u003C\u002Fcode>","\u003Ccode>self\u003C\u002Fcode> is the worker&#39;s global scope",[392,393,386],"Reply from a worker","\u003Ccode>self.postMessage(result)\u003C\u002Fcode>",[395,396,397],"Handle a worker crash","\u003Ccode>worker.onerror = (e) =&gt; {...}\u003C\u002Fcode>","Uncaught worker exceptions surface here, not as a thrown error",[399,400,401],"Stop a worker (from outside)","\u003Ccode>worker.terminate()\u003C\u002Fcode>","Immediate; no cleanup code inside the worker runs",[403,404,405],"Stop a worker (from inside)","\u003Ccode>self.close()\u003C\u002Fcode>","The worker finishes and exits","table",{"id":408,"code":409,"type":179,"language":180,"highlight":410},"b65","\u002F\u002F the whole pattern, copy-paste ready\nconst worker = new Worker(\"worker.js\");\n\nworker.postMessage({ type: \"RUN\", payload: someData });\n\nworker.onmessage = (event) => {\n  console.log(\"Result:\", event.data);\n};\n\nworker.onerror = (event) => {\n  console.error(\"Worker error:\", event.message);\n};\n\n\u002F\u002F later, when you're done with it\nworker.terminate();",[],{"id":412,"html":413,"type":135},"b66","\u003C!-- playground:start -->",{"id":415,"html":416,"text":416,"type":140,"level":31},"b67","🎮 Try it yourself",{"id":418,"html":419,"type":135},"b68","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":421,"html":422,"type":135},"b69","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":424,"html":425,"type":135},"b70","\u003C!-- playground:end -->",{"id":427,"html":428,"text":428,"type":140,"level":31},"b71","Key takeaways",{"id":430,"type":146,"items":431,"ordered":18},"b72",[432,433,434,435,436],"The browser has one thread for JavaScript and the DOM; anything expensive on it stalls painting, scrolling, and clicks.","A Web Worker is a separate thread with no shared memory — data crosses via \u003Ccode>postMessage\u003C\u002Fcode>, copied by the structured clone algorithm.","Functions and DOM nodes can never cross that boundary; only cloneable data can, unless you explicitly transfer a supported type like \u003Ccode>ArrayBuffer\u003C\u002Fcode>.","Always attach \u003Ccode>onerror\u003C\u002Fcode>, and always \u003Ccode>terminate()\u003C\u002Fcode> a worker you&#39;re done with — an uncleaned-up worker keeps running and holding memory.","Workers earn their overhead on substantial CPU-bound work, not on small or I\u002FO-bound tasks that were never blocking the main thread to begin with.",{"id":438,"html":439,"type":135},"b73","\u003C!-- quiz:start -->",{"id":441,"html":442,"text":442,"type":140,"level":31},"b74","🧠 Test yourself",{"id":444,"html":445,"type":135},"b75","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":447,"html":448,"type":135},"b76","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":450,"html":451,"type":135},"b77","\u003C!-- quiz:end -->",{"id":453,"html":454,"type":135},"b78","\u003Cp>That slider from the opening paragraph can stay responsive through the exact same 50,000-row parse — move the loop into a worker, send the rows over with \u003Ccode>postMessage\u003C\u002Fcode>, and the main thread never has a reason to stall. What&#39;s the heaviest loop in your own codebase that&#39;s still running where the UI can feel it?\u003C\u002Fp>",{"id":456,"type":457},"b79","divider",{"id":459,"html":460,"type":135},"b80","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":462,"html":463,"type":135},"b81","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":465,"type":146,"items":466,"ordered":18},"b82",[467,468,469],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Drag a slider on a page that's mid-way through parsing a 50,000-row CSV in JavaScript, and nothing happens for a full second. Not because the slider's code is slow — it never even runs. The browser's one JavaScript thread is busy with your parsing loop, and until that loop returns, no click, no scroll, no repaint gets a turn. The fix already ships in every browser you support: a second thread, called a Web Worker, that runs your code without ever touching the one thread the page's UI depends on.\n\n## What you'll learn\n\nBy the end of this guide you'll be able to:\n\n- Explain why the browser has exactly one thread for JavaScript and the DOM, and why that thread stalls the whole page under heavy work\n- Create a Web Worker, send it data, and get a result back without blocking the main thread\n- Reason correctly about what does and doesn't survive the trip between threads (structured cloning vs. transferable objects)\n- Handle worker errors, terminate workers cleanly, and avoid the memory leaks that come from forgetting to\n- Decide, with real judgment, when a worker is worth the complexity and when it isn't\n\n**Who this is for:** you've written `addEventListener` handlers and used `fetch`, and you've felt a UI stutter you couldn't explain.\n\n## Contents\n\n- [Why Web Workers exist](#why-web-workers-exist)\n- [The mental model](#the-mental-model)\n- [Building a worker, stage by stage](#building-a-worker-stage-by-stage)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## Why Web Workers exist\n\nJavaScript in the browser runs on a single thread — the **main thread** — and that same thread is also responsible for parsing HTML, computing styles, laying out and painting pixels, and responding to input. When your code runs, everything else waits. Here's the naïve version of the CSV problem from the opening line:\n\n```js\n\u002F\u002F the wrong way — this blocks everything else on the page\nfunction sumColumn(rows, columnIndex) {\n  let total = 0;\n  for (let i = 0; i \u003C rows.length; i++) {\n    total += Number(rows[i][columnIndex]);\n  }\n  return total; \u002F\u002F for 50,000+ rows, this can easily take 500ms–2s\n}\n\nbutton.addEventListener(\"click\", () => {\n  const total = sumColumn(hugeDataset, 3); \u002F\u002F main thread stalls here\n  resultEl.textContent = total;\n});\n```\n\nWhile `sumColumn` runs, the browser cannot repaint, cannot fire scroll or click handlers, and cannot update anything on screen — including a \"loading\" spinner you might have shown a moment earlier. The tab looks frozen because, for that stretch of time, it is. `setTimeout(fn, 0)` doesn't help either: it still runs the callback on the same main thread, just slightly later; it defers the freeze, it doesn't remove it.\n\nA Web Worker solves this by giving that expensive loop its own thread, with its own JavaScript engine instance, running in parallel with the main thread. The main thread stays free to paint and respond to input the entire time.\n\n## The mental model\n\n**The mental model:** a Web Worker is a separate JavaScript environment, running in parallel, that shares no memory with the page — the only way in or out is sending copies of data through a message channel.\n\nPicture two rooms with no shared furniture and no window between them, connected by a mail slot. You can pass a note through the slot (`postMessage`), and the other room can read it and mail one back (`onmessage`). Neither room can reach into the other and grab a variable, call a function, or touch a DOM element sitting in the other room — because nothing is actually shared. What crosses the slot is a **copy** of the data, produced by an algorithm called [structured cloning](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FStructured_clone_algorithm), not a reference to the original.\n\nThat single fact — no shared memory, only copied messages — explains almost every rule that follows: why a worker can't touch the DOM (the DOM objects live in the main thread's room), why you can't pass a function to a worker (functions aren't cloneable), and why very large payloads need a different trick (transferable objects, covered below).\n\n## Building a worker, stage by stage\n\n### Stage 1: the smallest working worker\n\nA worker is created from a separate script file:\n\n```js\n\u002F\u002F main.js\nconst worker = new Worker(\"sum-worker.js\");\n\nworker.postMessage({ rows: hugeDataset, columnIndex: 3 }); \u002F\u002F send a copy of the data\n\nworker.onmessage = (event) => {\n  resultEl.textContent = event.data.total; \u002F\u002F runs once the worker replies\n};\n```\n\n```js\n\u002F\u002F sum-worker.js — runs on its own thread\nself.onmessage = (event) => {\n  const { rows, columnIndex } = event.data;\n  let total = 0;\n  for (let i = 0; i \u003C rows.length; i++) {\n    total += Number(rows[i][columnIndex]);\n  }\n  self.postMessage({ total }); \u002F\u002F send the result back as a message\n};\n```\n\n> **Key concept:** the worker file has no access to `window` or the DOM — inside it, `self` refers to the worker's own global scope (`DedicatedWorkerGlobalScope`), not the page.\n\nClicking the button now posts the data to the worker and returns immediately; the main thread never blocks. The heavy loop runs on the worker's thread, and the page keeps painting and responding to input the whole time.\n\n### Stage 2: loading a worker without a separate file\n\nYou don't always want a second network request for a small worker script. You can build one from a string using a `Blob` and an object URL:\n\n```js\nconst workerSource = `\n  self.onmessage = (event) => {\n    const n = event.data;\n    self.postMessage(fib(n));\n  };\n  function fib(n) { return n \u003C 2 ? n : fib(n - 1) + fib(n - 2); }\n`;\n\nconst blob = new Blob([workerSource], { type: \"application\u002Fjavascript\" });\nconst worker = new Worker(URL.createObjectURL(blob));\n```\n\nThis is how the playground below builds its worker inline — useful for demos, small utility workers, or libraries that want to ship a worker without a second file to deploy.\n\n### Stage 3: module workers\n\nClassic workers load dependencies with the older `importScripts()` function. Modern browsers also support **module workers**, which use standard `import` statements, by passing `{ type: \"module\" }`:\n\n```js\nconst worker = new Worker(\"sum-worker.js\", { type: \"module\" });\n```\n\n```js\n\u002F\u002F sum-worker.js as a module\nimport { sumColumn } from \".\u002Fmath-utils.js\";\n\nself.onmessage = (event) => {\n  self.postMessage(sumColumn(event.data.rows, event.data.columnIndex));\n};\n```\n\nModule workers are supported in all current major browsers as of 2026. If you need to support an environment that predates module worker support, stick with a classic worker and `importScripts()`.\n\n### Stage 4: transferable objects, for when copying is too slow\n\nStructured cloning copies data. For a plain object with a few numbers, that copy is instant. For a 200 MB `ArrayBuffer` of audio or image data, copying it on every message becomes the new bottleneck. The fix is a **transferable object**: instead of copying an `ArrayBuffer`, you *transfer ownership* of it to the worker.\n\n```js\nconst buffer = new ArrayBuffer(1024 * 1024 * 50); \u002F\u002F 50 MB\nworker.postMessage(buffer, [buffer]); \u002F\u002F second argument: the transfer list\n\n\u002F\u002F after this call, `buffer.byteLength` on the main thread is 0 —\n\u002F\u002F the memory now belongs to the worker, not this thread\n```\n\n> **Key concept:** transferring moves the underlying memory instead of copying it, which is why it's effectively free even for huge buffers — and why the original reference becomes unusable afterward.\n\n`ArrayBuffer`, `MessagePort`, `ImageBitmap`, and a few stream types are transferable; plain objects, arrays, and strings are not — they're always copied.\n\n### Stage 5: cleanup and termination\n\nA worker keeps running (and keeps memory allocated) until you explicitly stop it:\n\n```js\n\u002F\u002F from the main thread\nworker.terminate(); \u002F\u002F stops the worker immediately, no matter what it's doing\n\n\u002F\u002F from inside the worker itself\nself.close(); \u002F\u002F the worker asks to stop itself, after finishing current work\n```\n\n`terminate()` is immediate and unconditional — any in-progress work in the worker is simply discarded, with no `finally` block guaranteed to run. Always terminate a worker you no longer need (e.g., when a component unmounts), or it keeps running and holding memory for the lifetime of the page.\n\n## Edge cases and gotchas\n\n- **No DOM access, ever.** A worker cannot read or write `document`, cannot use `window`, and cannot manipulate any DOM node you pass it — attempting to send a DOM node throws a `DataCloneError`, because DOM nodes are not structured-cloneable. If a worker needs to render something, it computes data and sends it back for the main thread to draw (or uses `OffscreenCanvas`, a separate, more advanced API).\n- **Functions can't cross the boundary.** You can't `postMessage` a callback and have the worker invoke it. Send data in, get data out; the worker's own script defines what it does with the data.\n- **Errors don't throw where you'd expect.** An uncaught exception inside a worker doesn't throw on the main thread — it fires an `error` event on the `Worker` object. Always attach a handler:\n\n  ```js\n  worker.onerror = (event) => {\n    console.error(\"Worker crashed:\", event.message, event.filename, event.lineno);\n  };\n  ```\n\n  Without this handler, a worker that throws simply goes silent from the main thread's point of view.\n- **Same-origin restriction.** A classic `new Worker(url)` script must be same-origin with the page (a `blob:` URL created by the page counts as same-origin for this purpose). You cannot point a worker directly at a third-party script URL.\n- **Workers aren't free to start.** Spinning one up has real overhead — allocating a thread, a new JS engine context, and loading the script. For a task that finishes in a few milliseconds, that overhead can cost more than the task itself. Workers pay off for work that's substantial or repeated, not for trivial one-off computations.\n- **A worker can spawn its own workers**, and can also use `fetch`, `WebSocket`, `setTimeout`, and `IndexedDB` — it's a real JavaScript environment, just without the DOM.\n- **`SharedWorker` is a different, less common API.** It allows multiple tabs from the same origin to connect to one shared worker instance via `port.postMessage`, instead of each tab getting its own dedicated worker. Check current browser support before relying on it — support has historically lagged `Worker` and `type: \"module\"`.\n\n## Best practices\n\n**Reach for a worker when** you have CPU-bound work that takes tens of milliseconds or more — parsing large files, running compression, image or audio processing, complex data transformations, cryptography, or search\u002Ffiltering over large in-memory datasets — especially if it needs to run while the user keeps interacting with the page.\n\n**Avoid it when** the work is already I\u002FO-bound (a `fetch` call doesn't block the main thread even without a worker — the wait for the network happens off-thread already), when the data involved is small enough that structured cloning would cost more than the computation itself, or when the task is short and infrequent enough that a worker's startup cost dominates.\n\n**Keep the message contract simple.** Design worker messages like a small API: a `type` field plus a payload, both directions. This scales cleanly to a worker that handles more than one kind of task.\n\n```js\nworker.postMessage({ type: \"PARSE_CSV\", payload: csvText });\nworker.postMessage({ type: \"SORT_ROWS\", payload: { rows, key: \"date\" } });\n```\n\n**Always pair creation with cleanup.** Every `new Worker(...)` should have a matching `terminate()` — in a component's unmount\u002Fcleanup hook, in a \"cancel\" button, or when the task naturally completes and you don't plan to reuse the worker.\n\n## FAQ\n\n### Can a Web Worker access the DOM?\n\nNo. Workers run in a context with no DOM APIs at all — no `document`, no `window`. If a worker needs something rendered, it sends data back to the main thread, which does the actual DOM update.\n\n### Do Web Workers block the main thread while they run?\n\nNo — that's their entire purpose. The worker's code executes on its own thread, in parallel with the main thread, so the page keeps painting and responding to input while the worker is busy.\n\n### Can I use `fetch` or `WebSocket` inside a worker?\n\nYes. Workers have access to `fetch`, `WebSocket`, `setTimeout`\u002F`setInterval`, `IndexedDB`, and `self.crypto`, among other APIs. What they lack is anything DOM-related.\n\n### What's the difference between a Web Worker and a Service Worker?\n\nA `Worker` is created and owned by one page for offloading computation and dies with that page (unless you use `SharedWorker`, which multiple tabs can connect to). A [`ServiceWorker`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FService_Worker_API) is a different API entirely: it's registered for an origin, keeps running independently of any open tab, and exists mainly to intercept network requests and enable offline support and push notifications. They solve different problems and aren't interchangeable.\n\n### Does `postMessage` copy my data or send a reference to it?\n\nBy default it copies, using the structured clone algorithm — the receiving side gets an independent copy, and mutating one side afterward never affects the other. The exception is objects you explicitly list in the transfer list (like `ArrayBuffer`), which move instead of copying.\n\n### Can I use React, Vue, or other framework code inside a worker?\n\nYou can run plain JavaScript logic (data transforms, computation, parsing) inside a worker just fine, but you cannot render framework components there, because rendering ultimately means touching the DOM, and workers have no DOM access. Keep workers for the computation; keep rendering on the main thread.\n\n## Cheat sheet\n\n| Task | Code | Notes |\n| --- | --- | --- |\n| Create a worker | `new Worker(\"file.js\")` | Script must be same-origin (or a `blob:` URL) |\n| Create a module worker | `new Worker(\"file.js\", { type: \"module\" })` | Lets the worker use `import` |\n| Send data to a worker | `worker.postMessage(data)` | Copies `data` via structured clone |\n| Transfer instead of copy | `worker.postMessage(buf, [buf])` | Only for transferable types (e.g. `ArrayBuffer`); `buf` becomes unusable on the sender's side afterward |\n| Receive data (main thread) | `worker.onmessage = (e) => e.data` | |\n| Receive data (inside worker) | `self.onmessage = (e) => e.data` | `self` is the worker's global scope |\n| Reply from a worker | `self.postMessage(result)` | |\n| Handle a worker crash | `worker.onerror = (e) => {...}` | Uncaught worker exceptions surface here, not as a thrown error |\n| Stop a worker (from outside) | `worker.terminate()` | Immediate; no cleanup code inside the worker runs |\n| Stop a worker (from inside) | `self.close()` | The worker finishes and exits |\n\n```js\n\u002F\u002F the whole pattern, copy-paste ready\nconst worker = new Worker(\"worker.js\");\n\nworker.postMessage({ type: \"RUN\", payload: someData });\n\nworker.onmessage = (event) => {\n  console.log(\"Result:\", event.data);\n};\n\nworker.onerror = (event) => {\n  console.error(\"Worker error:\", event.message);\n};\n\n\u002F\u002F later, when you're done with it\nworker.terminate();\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Key takeaways\n\n- The browser has one thread for JavaScript and the DOM; anything expensive on it stalls painting, scrolling, and clicks.\n- A Web Worker is a separate thread with no shared memory — data crosses via `postMessage`, copied by the structured clone algorithm.\n- Functions and DOM nodes can never cross that boundary; only cloneable data can, unless you explicitly transfer a supported type like `ArrayBuffer`.\n- Always attach `onerror`, and always `terminate()` a worker you're done with — an uncleaned-up worker keeps running and holding memory.\n- Workers earn their overhead on substantial CPU-bound work, not on small or I\u002FO-bound tasks that were never blocking the main thread to begin with.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nThat slider from the opening paragraph can stay responsive through the exact same 50,000-row parse — move the loop into a worker, send the rows over with `postMessage`, and the main thread never has a reason to stall. What's the heaviest loop in your own codebase that's still running where the UI can feel it?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":108,"canonical":472,"description":109},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-web-workers","01a018ca-67d5-7020-bce5-9c2b15decc45",{"name":475,"part":476,"total":476,"items":477},"Deep Dive",7,[478,483,488,493,497,501,505],{"slug":479,"title":480,"publishedAt":481,"readingMinutes":482},"weekly-javascript-event-loop","JavaScript Event Loop Explained: The Complete Guide","2026-07-13T09:36:33.464Z",13,{"slug":484,"title":485,"publishedAt":486,"readingMinutes":487},"weekly-typescript-generics","TypeScript Generics: The Complete Guide (with Cheat Sheet)","2026-07-20T09:23:23.621Z",15,{"slug":489,"title":490,"publishedAt":491,"readingMinutes":492},"weekly-cors-explained","CORS Explained: The Complete Guide (with Cheat Sheet)","2026-08-03T09:54:26.268Z",14,{"slug":494,"title":495,"publishedAt":496,"readingMinutes":487},"weekly-javascript-proxy-reflect","JavaScript Proxy and Reflect: The Complete Guide","2026-08-24T07:06:37.948Z",{"slug":498,"title":499,"publishedAt":500,"readingMinutes":492},"weekly-javascript-closures","JavaScript Closures: The Complete Guide (with Cheat Sheet)","2026-08-25T08:53:48.904Z",{"slug":502,"title":503,"publishedAt":504,"readingMinutes":492},"weekly-debounce-and-throttle","Debounce and Throttle in JavaScript: The Complete Guide","2026-08-31T13:12:14.139Z",{"slug":46,"title":108,"publishedAt":111,"readingMinutes":83},{"id":507,"locked":18},"01a018ca-6813-7291-ad02-c0965ab150d4",[509],{"id":45,"slug":46,"title":48,"_count":510},{"questions":51},[512],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":514,"questionCount":51},{"questions":51}]