Handling CPU-Intensive Work Using Web Workers In Angular
Tags: angular, javascript, webdev, learning
Published: 2020-08-07 14:40:43

---
title: Handling CPU-Intensive Work Using Web Workers In Angular
published: true
description: Web workers are great for improving the performance of web applications. They can be used in Angular to handle CPU-intensive tasks.
tags: angular, javascript, webdev, learning
canonical_url: https://blog.jscrambler.com/handling-cpu-intensive-work-using-web-workers-in-angular/
cover_image: https://blog.jscrambler.com/content/images/2020/08/jscrambler-blog-web-workers-in-angular.jpg
---

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:

```bash
ng g web-worker <worker-name>
``` 

From the [official docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers):

> 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:

```bash
ng new ang-web-worker
```
Then, navigate to the app project folder and start the app:

```bash
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](https://github.com/krispo/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:

```bash
npm install ng2-nvd3 
```
Add _NvD3Module_ to the _AppModule_ in `app.module.ts`:

```js
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:

```html
<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:

```js
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. 

```bash
npm start
```

You will have the Angular app running at `localhost:4200` displaying a bar chart.

![Angular Bar Chart](https://blog.jscrambler.com/content/images/2020/08/jscrambler-blog-web-workers-angular.png)

## 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`.

```js
  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:

```js
  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:

```bash
ng g web-worker canvas
```

The above command creates a file called `canvas.worker.ts`. Here is how it looks:

```js
/// <reference lib="webworker" />

addEventListener('message', ({ data }) => {
  const response = `worker response to ${data}`;
  postMessage(response);
});
```

Add the canvas code to the web worker:

```js
/// <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.

```js
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. 

```js
_worker.postMessage();
```
You need to pass the `OffscreenCanvas` to the worker thread. Let's create the canvas element and get an off-screen canvas.

```js
let  canvas = this.createCanvas();
document.getElementById('canContainer').append(canvas);
```

You need to pass the off screen canvas to the worker thread.

```js
let  offscreen = canvas.transferControlToOffscreen();
_worker.postMessage({canvas:  offscreen}, [offscreen]);
``` 

Here is how the complete `handleWebWorkerProcess` button event looks:

```js
  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](https://github.com/JscramblerBlog/web-worker-demo).

## 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) 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 <a href="https://blog.jscrambler.com/how-to-protect-angular-code-against-theft-and-reverse-engineering/?utm_source=dev.to&utm_medium=referral&utm_campaign=handling-cpu-intensive" target="_blank">this guide</a>.