There are times when you have to handle CPU-intensive tasks in web applications. CPU-intensive tasks can be anything like a complex calculation or some logic with too many iterations. Such tasks tend to make the web browser hang or lag until the task completes.
Why does the browser hang?
JavaScript is single-threaded. Whatever code you have written, it's executed in a synchronous manner. So, 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 other 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 the 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 in 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'll 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
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 and both 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 to 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 in 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 due to 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 the postMessage
on _worker
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 hanging the UI.
You can find the source code from this tutorial on GitHub.
Wrapping It Up
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 a difficult thing. 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 further about web workers.
Finally, 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 by following this guide.