Web Workers Don't Make JavaScript Faster — They Keep the UI Thread Breathing
A Bit Dry: Frontend Architecture Basics — What Exactly Is a Web Worker? Is It the Same as a Thread in Java?
I wonder if you've come across anything related to Web Worker before. Today, let's dig deep into this topic. I hope it helps.
Purpose
A Web Worker is essentially a newly opened thread. In most cases, the purpose of creating a Web Worker is to separate it from the main thread (UI thread).
The main thread (UI thread) is responsible for DOM rendering, page interactions, and general JS computations...
But when massive calculations occur on the main thread, the page becomes unresponsive.
At this point, you can open a new thread and place the main computation inside it.
This is the core purpose of a Web Worker.
A simple demo to create a Worker:
const worker = new Worker('./calc.worker.js')
worker.onmessage = (e) => {
console.log('Worker result:', e.data)
}
worker.postMessage({num: 10})
Create a new file named calc.worker.js:
self.onmessage = (e) => {
let sum = 0;
for (let i=0; i<e.data.num; i++) {
sum += i;
}
self.postMessage(sum)
}
There are several types of Workers, but this time we'll focus only on Dedicated Worker.
Besides this, there are also Shared Worker, Service Worker, AudioWorklet, CSS Painting Worklet, etc. (I'll leave a placeholder here and fill in the gaps later).
Differences
There are differences between the threads created by a Worker and actual operating system threads:
- Workers (Dedicated Workers) cannot share JS object memory; they lack the shared memory read/write operations found in Java. (
Shared Workeris a different story). - Worker threads are only for computation; they cannot manipulate the DOM and do not handle page interactions.
- Workers themselves are also affected by the browser's event loop, but they have their own event loop. You can simply think of the browser's event loop as the big circle, and the Worker's event loop as a small circle within it.
- Workers have an upper limit (more on this later).
From the example above, you should be able to tell that a Worker's operation is asynchronous.
The main thread sends tasks to the Worker via postMessage and receives the Worker's response through the onmessage callback.
Sending tasks and receiving results are not synchronous themselves; they are affected by the amount of computation, browser thread scheduling, task queuing, etc.
For a Dedicated Worker, it is bound to the context of the current page, and only that page can communicate with this Worker.
If the page is closed, this Worker is also destroyed directly.
Or you can manually destroy it by calling the worker.terminate() method.
Splitting Tasks
So, you, with your intellect occupying the high ground, will quickly think:
Can I just put all computational tasks into a Worker? That way, I can completely shield the main thread from computational overhead and let it focus solely on UI rendering.
To this, my answer is: Yes! But the result may not be what you want.
First, placing all computations in a Worker is feasible from a design perspective.
However, the problem lies in creating the Worker.
The creation of a Worker instance itself has a cold-start performance overhead, which is unavoidable.
When code creates a new Worker, it first needs to go through the browser's operating system thread to create an independent V8 Isolate (independent heap, independent JS environment).
Then it loads, parses, and compiles the worker.js script.
The whole process can take tens of milliseconds, or even hundreds of milliseconds.
Second, overhead also manifests in communication.
There is also overhead when communicating via postMessage. Even for small objects, there is an overhead of a few milliseconds.
This overhead is not a one-time cost; it occurs every time you communicate.
So, you can put all computational tasks in a Worker, but I don't recommend it!
It's not that computation is faster in a Worker. The core value of a Worker is that the computation does not happen on the main thread, thus avoiding blocking UI rendering.
For ordinary computations, the main thread actually outperforms the Worker because it lacks the creation & communication performance overhead.
Parallelism
You, with your intellect once again occupying the high ground, will certainly think quickly:
Since a Worker can "offload" computation, can I create multiple Workers to subdivide the computation, breaking down large calculations into smaller chunks to increase computation speed?
To this, my answer is: Yes! But use it in moderation.
The frontend itself can create multiple Workers to distribute computation. Each new Worker creates a child thread; theoretically, this is parallel computation.
But note that browsers have an upper limit on creating Workers. Chrome generally has a maximum of 8.
Moreover, the number of Workers is limited by the local device. If your computer has a 4-core 8-thread CPU, theoretically, you can open up to 8 Workers to compute simultaneously.
But if you create 20 Workers at the same time, they won't all run simultaneously.
Most Workers are just suspended, waiting for messages. Once they are all pushed to the CPU at once, it triggers OS thread scheduling queuing, and performance plummets.
Each Worker comes with its own independent V8 Isolate, which incurs significant memory overhead. A large number of Workers will consume a lot of memory.
Note here, I said "theoretically" you can create 8 Workers, but actually, you can't!
Because the operating system itself needs to take a portion of the CPU resources, other tasks need them too, and other browser tasks also need them. So the actual number of usable Workers available to you won't be that high.
We can use the navigator.hardwareConcurrency API to get the machine's logical core count, so we recommend creating:
Math.max(2, navigator.hardwareConcurrency - 1) Workers.
At this point, I believe you roughly understand the advantages and boundaries of Workers.
Teasing the Next Topic
But have you noticed that I've been talking about the CPU all along?
But hasn't mainstream computation now mostly moved to the GPU?
For example, AI training/inference, graphics rendering, etc.
So why isn't Worker computation done on the GPU to bypass the CPU's performance bottleneck?
We'll analyze this part in detail in the next article. If you're interested, please hit "Follow"! I would be very grateful!
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
The thread analogy is very intuitive!