Handling CPU-Intensive Work Using Web Workers in Angular
August 6th, 2020 | By Jay Raj | 6 min read
Learn how to handle CPU-Intensive Work using Web Workers in Angular to improve the performance of web apps.
There are times when you have to handle CPU-intensive tasks in web applications. CPU-intensive tasks can be anything from a complex calculation to some logic with too many iterations. Such tasks tend to make the web browser hang or lag until the task is complete.
Why does the browser hang?
JavaScript is single-threaded. Whatever code you have written is executed synchronously. Thus, if a task or a piece of code is taking time to complete, the browser freezes until it finishes. Only one thing is executed at a time on the single main thread.
Introduction to Web Workers
Web workers are great for making web applications fast. They make the application fast by running CPU-intensive tasks on a different thread than the main thread.
Angular has added support for web workers in Angular version 8 and later. CLI support has been added to create web workers from the Angular CLI.
You can create a web worker by using the following CLI command:
ng g web-worker <worker-name>
From FrontendMasters official docs:
Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
The main thread and the worker thread communicate by posting messages to an event handler.
Creating the basic app skeleton
Assuming that you already have the Angular CLI installed, let's create an Angular app:
ng new ang-web-worker
Then, navigate to the app project folder and start the app.
cd ang-web-worker
npm start
You will have the web app running at localhost:4200.
Let's now create a task that updates a graph at 1-second intervals. It'll help in observing the performance improvement provided by the web worker.
For the sake of this tutorial, let's use ng2-nvd3 for creating a graph in Angular.
We will update the graph data in 1-second intervals. Along with the graph update, we'll add another task to create rectangles in the canvas using the main thread and also using the web worker.
Install the ng2-nvd3 module in the project.
npm install ng2-nvd3
Add NvD3Module to the AppModule in app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NvD3Module } from 'ng2-nvd3';
import { HttpClientModule } from '@angular/common/http';
import 'd3';
import 'nvd3';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
NvD3Module,
HttpClientModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now, let's add some code to the app.component.html file:
<div class="main">
<div class="graph">
<nvd3 [options]="options" [data]="data"></nvd3>
</div>
<div class="container">
<div>
<input type="button" (click)="handleButtonClick()" value="Main Thread Task" />
<input type="button" (click)="handleWebWorkerProcess()" value="Web Worker Task" />
</div>
<div id="canContainer" class="canvasContainer">
</div>
</div>
</div>
Let's also modify the app.component.ts file. Here is how it looks:
import { Component,OnInit, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
declare let d3: any;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css', '../../node_modules/nvd3/build/nv.d3.css'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
title = 'nvd3-graph';
options;
data;
constructor(){}
ngOnInit() {
this.initChart();
setInterval(()=>{
this.updateChart();
}, 500)
}
initChart(){
this.options = {
chart: {
type: 'discreteBarChart',
height: 450,
x: function(d){return d.label;},
y: function(d){return d.value;},
showValues: true,
valueFormat: function(d){
return d3.format(',.4f')(d);
},
duration: 500,
xAxis: {
axisLabel: 'X Axis'
},
yAxis: {
axisLabel: 'Y Axis',
axisLabelDistance: -10
}
}
}
}
updateChart()
{
this.data = [
{
values: [
{
"label" : "A" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "B" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "C" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "D" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "E" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "F" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "G" ,
"value" : Math.floor(Math.random() * 100)
} ,
{
"label" : "H" ,
"value" : Math.floor(Math.random() * 100)
}
]
}
];
}
handleButtonClick(){
}
handleWebWorkerProcess(){
}
clearCanvas(){
let element = <HTMLCanvasElement> document.getElementById('canContainer');
element.innerHTML = ''
}
}
Make sure to modify the target in compilerOptions to es5 in tsconfig.json, or it might not work. Save the above changes and start the app.
npm start
You will have the Angular app running at localhost:4200 and displaying a bar chart.
Processing the CPU-intensive task in the Main UI thread
As seen in the above screenshot, the app contains two buttons, both of which accomplish the same task: drawing on a canvas. One will make use of the main thread, and the other will make use of a web worker.
Let's add the code to run the task in the main UI thread. Start by creating the canvas element in app.component.ts.
createCanvas(){
let canvas = document.createElement('canvas');
canvas.setAttribute('width','700');
canvas.setAttribute('height','500');
return canvas;
}
Once you have the context for the canvas, create 10x10px rectangles to fill the canvas, which is 700px by 500px.
Here is how the handleButtonClick handler looks:
handleButtonClick(){
this.clearCanvas();
let canvas = this.createCanvas();
document.getElementById('canContainer').append(canvas);
let context = canvas.getContext("2d");
context.beginPath();
for(let x = 0; x < 691; x++){
for(let y = 0; y < 491; y++){
context.fillRect(x, y, 10, 10);
}
}
}
Save the above changes. You will notice that the graph is updating at frequent intervals.
Upon clicking the Main Thread Task button, the UI hangs for a couple of seconds, and then the graph update continues. That delay was caused by the time-consuming canvas-writing task.
Processing the CPU-intensive task in a web worker
Now, let's see how you can solve the UI lag issue caused by the CPU-intensive canvas writing task. Let's create a web worker in your Angular project using the following command:
ng g web-worker canvas
The above command creates a file called canvas.worker.ts. Here is how it looks:
/// <reference lib="webworker" />
addEventListener('message', ({ data }) => {
const response = `worker response to ${data}`;
postMessage(response);
});
Add the canvas code to the web worker:
/// <reference lib="webworker" />
addEventListener('message', ({ data }) => {
let canvas = data.canvas;
let context = canvas.getContext("2d");
context.beginPath();
for(let x = 0; x < 691; x++){
for(let y = 0; y < 491; y++){
context.fillRect(x, y, 10, 10);
}
}
});
Note: If you have a more powerful CPU and are unable to see the UI getting stuck, feel free to increase the x and y ranges from 691 and 491, respectively, to a higher range.
For the web worker to write to the canvas, you need to make use of the OffscreenCanvas API. It decouples the canvas API and DOM and can be used in a web worker, unlike the canvas element.
Let's add the code to create a worker thread using the canvas.worker.ts file.
let _worker = new Worker("./canvas.worker", { type: 'module' });
Once you have the worker instance created, you need to attach an onmessage handler to the worker.
To get the worker started, you need to call postMessage on _worker instance.
_worker.postMessage();
You need to pass the OffscreenCanvas to the worker thread. Let's create the canvas element and get an off-screen canvas.
let canvas = this.createCanvas();
document.getElementById('canContainer').append(canvas);
You need to pass the off-screen canvas to the worker thread.
let offscreen = canvas.transferControlToOffscreen();
_worker.postMessage({canvas: offscreen}, [offscreen]);
Here is how the complete handleWebWorkerProcess button event looks:
handleWebWorkerProcess(){
this.clearCanvas();
let canvas = this.createCanvas();
document.getElementById('canContainer').append(canvas);
let offscreen = canvas.transferControlToOffscreen();
let _worker = new Worker("./canvas.worker", { type: 'module' });
_worker.onmessage = ({ data }) => {
console.log(data);
};
_worker.postMessage({canvas: offscreen}, [offscreen]);
}
Save the above changes and restart the app.
You should now see the graph updating at an interval of 500 ms. You can observe that clicking on the Main Thread Task button hangs the UI since it's running the task on the main thread.
However, clicking on the Web Worker Task button runs the task in another thread without changing the UI.
You can find the source code for this tutorial on the GitHub repository, along with a demo of Web Worker in Angular.
Conclusion
In this tutorial, you learned how to handle CPU-intensive tasks using web workers in Angular.
Before web workers came into existence, running time-consuming tasks in the browser was difficult. With web workers, you can run any long-running task in parallel without blocking the main UI thread.
What we discussed in this tutorial is just the tip of the iceberg. I recommend reading the official documentation to learn more about web workers.
Don't forget to pay special attention if you're developing commercial Angular apps that contain sensitive logic.
You can protect them against code theft, tampering, and reverse engineering.
Jscrambler
The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.
View All ArticlesMust read next
Web Workers And The Threads That Bind Us
Check how Web Workers can help improve the browser performance and allow users to have a better experience
June 30, 2016 | By Jscrambler | 8 min read
Building an app with Angular & Firebase
In this tutorial, you will learn how to build a basic CRUD app using Angular and Firebase
August 26, 2021 | By Jay Raj | 11 min read