Improve UI performance using Web Worker
JavaScript is single-threaded, it executes a single line of code at a time synchronously. We have to perform intensive tasks and we want the UI to run smoothly at 60fps. App gets 16 milliseconds to run JavaScript code and render DOM again. So if we run a CPU intensive task then it will keep the UI thread busy and your application becomes unresponsive during that time. We can solve this problem using Web Worker.
What is Web Worker?
A Web Worker creates a parallel execution environment separate from your main thread to run your script. It allows you to outsource your heavy computational task without blocking your main thread. Web Worker doesn’t have access to the web page & DOM API. Web Worker has high startup performance cost and high per-instance memory cost, so we use workers where it is expected to be a long-lived or heavy computational task.
Working with the Web Worker is very straightforward. First, you need to create a JavaScript file containing your part of the application that you want your web worker to execute. Browsers provide Worker API to create an instance of the Worker object, passing the path of the worker file.
const webWorker = new Worker(‘worker.js’)
Once worker instance is created, to communicate with a worker we use postMessage() to send data to worker thread.
webWorker.postMessage(someData)
To receive data from the main thread to worker, we declare onmessage function or add an event listener.
// worker.js
self.onmessage = function(event) {
console.log(`Message from main thread ${event.data}`);
// event.data = someData
}
// or
self.addEventListener("message", (event) => {
console.log(`Message from main thread ${event.data}`);
}
Similarly to send data from worker to main thread we use self.postMessage() and to receive data at the main thread we declare onmessage or add an event listener.
// worker.js
self.postMessage(someOtherData)
// main.js
webWorker.addEventListener("message" , (event) => {
console.log(`Message from worker ${event.data}`);
// event.data = someOtherData
}
Once you are done with the worker, you can terminate by calling a terminate() function on your worker object or you can terminate worker from inside worker too using self.close()
// worker.js
self.close();
// main.js
webWorker.terminate();
In case of error occurs in a web worker, ErrorEvent is fired. ErrorEvent can be listened to by adding an event listener to the worker object.
// main.js
webWorker.addEventListener("error", (error)=> {
console.log('error in web worker-> ' , error.message);
});
We can also import external scripts inside web worker using global function importScripts(), it accepts zero or more URIs as parameters.
// worker.js
importScripts('imageProcessing.js') // single import
importScripts('imageProcessing.js', 'enc-aes.js') // multiple import
Simple and easy right, Let checkout simple example and compare the performance.
// main.js <without web worker>
function task(n) {
for (let i = 1; i <= n; i++);
}
task(10000000000);
The above script took about 16s to run in my machine(Intel® Core™ i7–8550U CPU @ 1.80GHz × 8). Now check the same example with a web worker.
// main.js
const worker = new Worker('worker.js');
worker.postMessage(1000000000);
worker.addEventListener('message', ({ data }) =>{
worker.terminate();
});
// worker.js
function task(n) {
for (let i = 1; i <= n; i++);
}
self.onmessage = function({ data }) {
if (data) {
task(parseInt(data));
self.postMessage('');
}
};
It took about 2s to perform the same task with the web worker.
As I already mentioned earlier that web worker has a high startup performance cost, we can test that with the above example by reducing the value of n. if we compare the time performance with n = 100000, without web worker it took about 3ms and with a web worker, it took about 52ms. As you can see using web worker for smaller tasks is not an optimal solution.
Why use Web Worker?
- Improve your application performance and enhances user experience.
- Application logic doesn’t block UI thread.
- Web Worker will allow you to utilize multiple cores of the client’s machine.
- Supported by all modern browsers (IE 10+).
Limitation of Web Worker?
- No Access to DOM API.
- It shares no memory with UI thread.
- It increases the CPU & Memory utilization of the client’s machine.
- Web Worker script should be served from the same domain and protocol
Now we know how web worker works and its strength and limitation. Here are some use cases of web worker which you can use to improve your application performance.
- Encryption/Decryption: At NoBroker we take security very seriously. Internal APIs at NoBroker are encrypted & have to decrypt them at the front end. In Dashboard, we have a large set of data coming from API so the decryption process takes time which increases loading time. So we move the decryption process to a web worker as no DOM API is required. As we know web worker has high startup cost, so we initiate web worker at dashboard load and keep the instance of web worker available through the app, so we don’t have to waste time every time we want to decrypt data.
- Background Polling: Web worker is very useful for background polling and updates the data. We can process new data at the web worker and push the update to our app without adding extra load to UI thread.
- Audio or Image Processing: Image or Audio processing both are CPU intensive tasks if we try to perform those operations in the main UI thread then it will make your application unresponsive. All the computational logic can be added to a Web Worker to avoid blocking the UI thread. You can split your tasks into multiple workers and fully utilize multiple cores and increase the speed of processing of data.
- Spell Checker: For the spell check program, we have a dictionary containing correct words stored in the trie data structure to make search efficient. When the input is provided to the program, we have to look for an exact match or match which is close to that word. All these processing we can offload to web workers without making UI thread busy.
- Prefetching Data: To optimize and improve the speed of your app, we can prefetch data in advance and store it using a web worker. So when later we need that data so we can retrieve that data from web worker instead of making APIs call, which saves load time. For example at NoBroker when we search for property in a particular area we fetch a subset of the entire result. To reduce the load time on the click of the next page, we prefetch.
That’s a lot of places and a lot more where we can use the power of web workers to make the JS work in the parallel thread and make the most of the JS in enhancing the web performances.