AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Web Workers: Multi-threading in JavaScript

📚 Optimization⏱️ 24 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

The Frozen Submit Button

You have almost certainly lived through this moment. You are filling out a form on a school portal, a bank site, or a ticket-booking page. You press "Submit." Nothing happens. The spinner does not spin. The page does not scroll. Even the browser's own clock in the corner of your screen seems to freeze for a second. Then, all at once, everything catches up — your click registers, the page updates, and it is as if the freeze never happened.

Most students assume this is "just the internet being slow." Sometimes it is. But very often, the real cause has nothing to do with your network connection at all. It is happening entirely inside your own browser, on your own device, because of a design decision baked into JavaScript itself: JavaScript runs on a single thread. One thread means one worker doing one job at a time — and if that job is long, everything else, including the parts of the page that are supposed to react to your clicks, has to wait in line behind it.

This chapter is about the tool JavaScript gives you to fix exactly this problem: the Web Worker. By the end, you will understand not just how to use one, but precisely why it is needed, what it can and cannot do, and how to reason about when a program should reach for it.

Why Does the Page Freeze? JavaScript's One-Lane Road

Think of a small shop with a single cashier and a single billing counter. Ten customers can stand in line, but only one bill can be processed at a time. If customer number three has a complicated return-and-exchange request that takes five minutes, customer number four — who just wants to pay ₹20 for a pack of biscuits — has no choice but to wait, even though their transaction would take two seconds.

A JavaScript program in a web browser works the same way. There is exactly one main thread — one "cashier" — and it is responsible for everything: running your JavaScript code, calculating what the page should look like, painting pixels on the screen, and responding to your clicks, taps, and key presses. All of these tasks are lined up and handled one after another, never simultaneously. If one task — say, a JavaScript function performing a huge calculation — takes three seconds to finish, the main thread cannot paint a new frame, cannot process your click, and cannot do anything else until that function returns control. The browser does not crash; it is simply busy, exactly like the cashier who hasn't abandoned the register, just hasn't gotten to you yet.

This is the single most important fact to internalize before writing a single line of Web Worker code: a normal JavaScript program has only one lane, and every task — rendering, computing, and reacting to input — must take turns using it.

A Number Puzzle First: Testing for Primes by Hand

To make "a long-running calculation" concrete rather than abstract, let's build one you can trace by hand: testing whether a number is prime.

A number is prime if it has no divisors other than 1 and itself. The simplest way to test this is called trial division: try dividing the number by every integer from 2 up to its square root. If none of them divide evenly, the number is prime. You only need to check up to the square root because if a number n has a divisor larger than √n, it must also have a matching divisor smaller than √n — so if none of the small ones work, none of the large ones can either.

Let's test 29. Since √29 ≈ 5.39, we only need to check divisors 2, 3, 4, and 5:

  • 29 ÷ 2 → remainder 1
  • 29 ÷ 3 → remainder 2
  • 29 ÷ 4 → remainder 1
  • 29 ÷ 5 → remainder 4

No remainder was zero, so 29 is prime.

Now test 91. Since √91 ≈ 9.54, we check 2 through 9:

  • 91 ÷ 2, ÷3, ÷4, ÷5, ÷6 → all leave a remainder
  • 91 ÷ 7 → remainder 0. Stop — 91 = 7 × 13, so 91 is not prime.

This little procedure — loop from 2 to √n, check for a zero remainder — is exactly what we will turn into code. And here is the part that matters for this chapter: testing one number like 91 is instant. But testing every number from 2 up to, say, five million, one after another, means running this loop millions of times in a row. That is no longer instant — and that is precisely the kind of task that will freeze our page.

Turning the Puzzle Into Code — and Watching It Block Everything

Here is the trial-division test as a JavaScript function, followed by a page with two buttons: one that counts primes below a large limit, and one that simply changes the background colour — something that should feel instant.

<button id="calcBtn">Count primes below 5,000,000</button>
<button id="colorBtn">Turn background green</button>
<p id="output"></p>

<script>
function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i * i <= n; i++) {
    if (n % i === 0) return false;
  }
  return true;
}

document.getElementById("calcBtn").addEventListener("click", () => {
  let count = 0;
  for (let i = 2; i < 5000000; i++) {
    if (isPrime(i)) count++;
  }
  document.getElementById("output").textContent = "Primes found: " + count;
});

document.getElementById("colorBtn").addEventListener("click", () => {
  document.body.style.background = "lightgreen";
});
</script>

Trace what happens when you click "Count primes." The click handler starts running on the main thread. Inside it, a for loop runs from 2 to 5,000,000, and for every single one of those roughly five million numbers, it calls isPrime, which itself contains another loop (checking divisors up to √n). This is many millions of individual operations, all executing back-to-back on the one and only thread the browser has for running your code.

Now click "Turn background green" while that loop is still running. Nothing happens — not because the click was ignored, but because the event has been queued up, waiting its turn, and the main thread is still stuck inside the for loop from the first click. Only after the loop finally finishes and the first handler returns does the browser get a chance to look at its queue, see the pending click, and run the second handler. The colour change appears late, all at once, exactly like the frozen submit button from the introduction.

The 16.7-Millisecond Deadline

There is a precise number behind "the page feels frozen," and it is worth doing the arithmetic once. For an animation or a scroll to look smooth to the human eye, browsers try to redraw the screen 60 times every second — 60 frames per second. That gives a budget per frame of:

1000 ms ÷ 60 ≈ 16.7 ms

Every 16.7 milliseconds, the browser needs a free moment on the main thread to calculate and paint the next frame. If your JavaScript occupies the main thread continuously for longer than that — say, for 3,000 milliseconds while counting primes — the browser simply cannot fit in any of the roughly 180 frames it would normally have painted during that time. The page doesn't crash or lag slightly; it stops updating altogether until your code hands control back. This is the real, measurable meaning of "the page froze."

Concurrency Is Not Parallelism

Before introducing Web Workers, it's worth heading off a very common confusion. You may already know that JavaScript has tools like setTimeout, promises, and async/await that let a program "wait" for something (like a network response) without freezing the page. This is real and useful — but it is not multi-threading.

Those tools work by having the main thread briefly pause a task, go do something else, and come back to it later — all still on the same single thread, just interleaved cleverly. This is called concurrency: many tasks making progress by taking turns. It solves the problem of waiting (for a network reply, a timer, a file), because while waiting, the thread is genuinely free to do other things.

It does not solve the problem of a CPU-heavy calculation like our prime-counting loop, because that loop is never "waiting" for anything — it is constantly, continuously using the CPU. No amount of async/await rearranging will make that loop stop blocking the thread, because there is still only one thread, and it is busy the entire time. To actually run two pieces of CPU-bound work at the same time, on two different CPU cores, you need genuine parallelism — a second thread. That is exactly what a Web Worker gives you.

Enter the Web Worker: A Second Lane

A Web Worker is a separate JavaScript execution environment that your page can create, which runs on its own operating-system thread — genuinely in parallel with your main thread, often on a different CPU core. It has its own global scope, its own call stack, and runs its own script file, completely independent of the main thread's business.

Going back to the shop analogy: a Web Worker is like opening a second billing counter with its own cashier. The complicated return-and-exchange customer can now be handled at counter two, while counter one keeps serving quick customers without any delay. Crucially, the two cashiers don't share a cash drawer — they can pass notes to each other, but they aren't reaching into the same till at the same time. That distinction — separate workers that communicate by passing messages rather than sharing memory — is the single most important design rule of the Web Worker API, and we'll return to it shortly.

Building It: Main Script and Worker Script

Let's rebuild the prime-counting example using a worker. This requires two files: the main script (which runs on the main thread, as usual) and a separate worker script (which the browser will run on its own thread).

primeWorker.js — this file only ever runs inside the worker thread:

function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i * i <= n; i++) {
    if (n % i === 0) return false;
  }
  return true;
}

self.onmessage = function (event) {
  const limit = event.data.limit;
  let count = 0;
  for (let i = 2; i < limit; i++) {
    if (isPrime(i)) count++;
  }
  self.postMessage({ count: count });
};

main.js — this stays on the main thread:

const worker = new Worker("primeWorker.js");

document.getElementById("calcBtn").addEventListener("click", () => {
  worker.postMessage({ limit: 5000000 });
  document.getElementById("output").textContent = "Counting in the background...";
});

worker.onmessage = function (event) {
  document.getElementById("output").textContent = "Primes found: " + event.data.count;
};

document.getElementById("colorBtn").addEventListener("click", () => {
  document.body.style.background = "lightgreen";
});

Now trace it carefully, step by step, because the order of events is the whole point:

  1. new Worker("primeWorker.js") tells the browser to start a brand-new thread and load primeWorker.js into it. That worker thread sits idle, waiting, having registered self.onmessage.
  2. You click "Count primes." The main thread's handler runs — but notice it does almost nothing itself. It calls worker.postMessage({ limit: 5000000 }), which packages up that data and sends it to the worker thread, then immediately returns. The main thread's click handler is finished in a fraction of a millisecond.
  3. Over on the worker thread, receiving that message triggers self.onmessage. Now the million-iteration loop begins — but it is running on the worker's own thread, not the main thread.
  4. While the worker is busy counting, the main thread is completely free. If you click "Turn background green" right now, it runs instantly — no queueing, no waiting — because the main thread was never blocked in the first place.
  5. Eventually the worker's loop finishes, and it calls self.postMessage({ count: count }), sending the result back to the main thread.
  6. This triggers worker.onmessage back on the main thread, which updates the page text with the final count.

The total amount of computation is identical to the blocking version — every one of those five million numbers is still tested exactly the same way. What has changed is where that computation happens, and consequently, whether it blocks anything else.

What Actually Gets Sent? The Structured Clone

Misconception to correct directly: many students assume that because a worker is "just another thread in the same program," it must share variables and objects with the main thread the way threads do in languages like Java or C++. This is false for standard Web Workers.

When you call postMessage(data), the browser does not hand the worker a live reference to your object. It runs the structured clone algorithm, which makes a full, independent copy of the data and delivers that copy to the other side. If the main thread later changes the original object, the worker's copy is completely unaffected, and vice versa. This is deliberate: it prevents the classic, hard-to-debug bugs of traditional multi-threaded programming, where two threads racing to read and write the exact same memory location produce unpredictable results depending on timing.

The cost is that copying large amounts of data back and forth isn't free — for genuinely huge datasets, the copying itself can take real time. (Advanced note, not needed for basic use: certain objects like ArrayBuffer can instead be transferred — ownership moves to the worker with no copy — but that is a specialised optimisation, not the default behaviour.)

What Workers Cannot Do

Second misconception to correct: a worker cannot touch the page at all. There is no document, no window, and no access to the DOM inside a worker's global scope. If you write document.getElementById(...) inside primeWorker.js, it will throw an error immediately — document simply does not exist there. This is not a limitation someone forgot to remove; it is a deliberate safety rule, because the DOM itself is not thread-safe. Allowing two threads to modify the same page structure simultaneously would reopen exactly the kind of unpredictable bugs the "no shared memory" rule was designed to avoid.

A worker's job is to compute a result and hand it back via postMessage. Only the main thread is ever allowed to update what the user actually sees.

Managing Workers: Multiple Workers, Errors, and Shutting Down

You are not limited to one worker. A page can create several, each running its own script on its own thread, useful for splitting one big job into pieces (for example, having four workers each check a different slice of the number range and report back a partial count). A rough guide to how many threads your device can genuinely run in parallel is navigator.hardwareConcurrency, which reports the number of logical processor cores available — you might see 4 or 8, depending on the device.

Workers can fail — a bug in the worker's script throws an error just like any other JavaScript error. You can catch this on the main thread with worker.onerror, which receives an event describing the message, file, and line number where it happened, rather than letting the failure vanish silently in the background thread.

When a worker is no longer needed, it should be shut down, since an idle worker still occupies system resources. The main thread can force this with worker.terminate(), which stops the worker immediately, wherever it is in its execution. A worker can also close itself gracefully from the inside by calling self.close() once it has finished its task and sent its final message.

Seeing It on a Timeline

The diagram below compares both versions side by side: the single blocked thread on top, and the two parallel threads on the bottom.

Without a Web Worker — one thread must do everything, in order you click a 2nd button here renders fine isPrime() loop running — nothing else can happen queued click finally runs ...but it waits here until the red block ends With a Web Worker — two threads run at the same time Main thread (page + clicks) click handled instantly postMessage(limit) sent to worker postMessage(count) sent back Worker thread (background) isPrime() loop runs here — same work, different thread Same amount of computation — but the main thread never freezes, because the heavy loop now runs on a separate thread.

Web Workers vs Service Workers — Don't Confuse Them

One more mix-up worth heading off: a Service Worker is a different, unrelated API that also happens to run in the background. Its job is to intercept network requests your page makes — for example, to cache files so a site keeps working offline, or to enable push notifications. It is not primarily a tool for parallel computation, and it does not exist to speed up a slow loop. A (Dedicated) Web Worker, the subject of this chapter, exists specifically to run ordinary JavaScript computation on a background thread. Similar name, similar "runs in the background" flavour, completely different purpose — treat them as separate tools that happen to share a naming pattern.

When to Use a Worker (and When Not To)

Creating a worker and passing messages to it has real overhead — starting the thread takes time, and every postMessage call involves cloning data. This means workers are the right tool specifically for CPU-bound work: long-running calculations that keep the processor busy continuously, such as searching large datasets, running simulations, processing images pixel by pixel, or — as in our example — testing millions of numbers for primality.

They are the wrong tool for ordinary waiting, such as fetching data from a server. That kind of work is already I/O-bound, not CPU-bound — the thread isn't busy calculating, it's idle while waiting for a reply — and JavaScript's existing promise-based tools handle that efficiently without needing a second thread at all. Reaching for a worker to wrap a simple network request adds complexity and copying overhead for no real benefit. The decision rule is simple: if a task will keep the CPU pinned for more than roughly one frame's worth of time (that 16.7 ms budget) and doesn't involve the DOM, it's a strong candidate for a worker. If it mostly involves waiting, it isn't.

Summary

JavaScript's main thread is a single lane shared by your code, page rendering, and every user interaction — a long CPU-bound task on that thread blocks everything else until it finishes, which is why heavy calculations make pages feel frozen. Tools like async/await manage waiting efficiently but do not add a second thread, so they cannot prevent this kind of freeze. A Web Worker genuinely does add a second thread: created with new Worker("file.js"), it runs its own script in parallel, communicating with the main thread only through postMessage/onmessage, with data copied via the structured clone algorithm rather than shared directly. Workers have no access to the DOM, can be created in multiples, can fail (caught via onerror), and can be shut down with terminate() or self.close(). They are the correct fix specifically for CPU-heavy work — not for network waiting, which async code already handles.

Check Your Understanding

  1. Explain, in your own words, why clicking a second button during a long for loop on the main thread doesn't run the second handler until the loop finishes — even though the two pieces of code have nothing to do with each other.
  2. By hand, using trial division, determine whether 97 is prime. State every divisor you needed to check and why you could stop where you did.
  3. A page performs 60 frames per second when idle. If a single JavaScript function runs for exactly 250 ms without pausing, approximately how many frames does the browser miss during that time? Show the arithmetic.
  4. A classmate says: "I used await before my heavy calculation, so now it runs on a different thread and won't block the page." Explain precisely what is wrong with this statement.
  5. In the worked worker example, suppose primeWorker.js mistakenly contained the line document.title = "done"; right after computing count. What would happen when the worker tried to run that line, and why?
  6. Trace this modified version of main.js and predict, in order, everything that gets printed to the page, assuming the worker takes about 3 seconds to finish counting: the "Counting in the background..." click happens at t = 0s, and the "Turn background green" button is clicked at t = 1s.

Think About It

Think about this: How would you explain web workers: multi-threading in javascript to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where web workers: multi-threading in javascript is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting web workers: multi-threading in javascript to at least 3 other topics you have studied.
← Memory Leaks: Debugging and PreventionAccessibility: Building Inclusive Web Apps →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn