Running Web Worker in Angular 6 Application — A step-by-step guide

Suresh Patidar
8 min readOct 6, 2018

--

In my previous article, we talked about the importance of web application performance and how web worker can help building responsive web applications.

In this article we are going to discuss about different ways to integrate web workers in your angular 6 application.

There are two popular ways to integrate Web Worker in your angular application:

  1. Run all the business logic of your application in a web worker thread.
  2. Run some of CPU intensive business logic in a web worker thread.

Angular has provided support for approach #1 in the framework itself. In order to work with this approach your application code should not have any direct reference to DOM, Window, Parent and document in TS files because this code is going to run in web worker thread and these objects are not available in worker thread context.

This approach works well if you are starting new application or the existing application has been designed with this requirement in mind.

I have written another article on LinkedIn that talks about approach #1 in detail with sample code available on GitHub.

If you have not planned for web worker from beginning of your project and not sure if your project is compatible to run in worker thread then you can proceed with approach #2 i.e. running some of your business logic in web worker thread.

This approach doesn’t require any change/refactoring in your existing code and provide with a better control to choose what code/logic to run in web worker thread and what in main thread.

This article describes the approach #2 in detail.

Prerequisite:

I am assuming that you are using your existing Angular 6 application created using CLI 6 to integrate web worker with. To write sample code for this article I have created an new Angular 6 application using CLI 6 but this should not matter as the steps below are same for both new and existing application.

For simulating CPU intensive processing I am going to create a mock function that you can replace with your real business logic.

Step-by-Step guide:

  1. Create basic code and skeleton for our web worker
  • Create a new folder called worker under src to hold all the logic for web worker. Create main.worker.ts in worker directory to serve as entry point for our web worker.
/* <project-root>/src/worker/main.worker.ts */import { AppWorkers } from './app-workers/app.workers';export const worker = new AppWorkers(self);addEventListener('message', ($event: MessageEvent) => {
worker.workerBroker($event);
});
  • Create a folder named app-workers under worker. Create app.workers.ts under app-workers that will hold logic to distribute work based on message topic.
/* <project-root>/src/worker/app-workers/app.workers.ts */import { MockedCpuIntensiveWorker } from './mocked-cpu-intensive.worker';
import { WorkerMessage } from './shared/worker-message.model';
import { WORKER_TOPIC } from './shared/worker-topic.constants';
export class AppWorkers {
workerCtx: any;
created: Date;
constructor(workerCtx: any) {
this.workerCtx = workerCtx;
this.created = new Date();
}
workerBroker($event: MessageEvent): void {
const { topic, data } = $event.data as WorkerMessage;
const workerMessage = new WorkerMessage(topic, data);
switch (topic) {
case WORKER_TOPIC.cpuIntensive:
this.returnWorkResults(MockedCpuIntensiveWorker.doWork(workerMessage));
break;
default: // Add support for other workers here
console.error('Topic Does Not Match');
}
}
private returnWorkResults(message: WorkerMessage): void {
this.workerCtx.postMessage(message);
}
}
  • Create shared folder under app-workers to hold shared model and constants. Create worker-message.model.t under shared directory.
/* <project-root>/src/worker/app-workers/shared/worker-message.model.ts */export class WorkerMessage {
topic: string;
data: any;
constructor(topic: string, data: any) {
this.topic = topic;
this.data = data;
}
public static getInstance(value: any): WorkerMessage {
const { topic, data } = value;
return new WorkerMessage(topic, data);
}
}
  • Create worker-topic.constants.ts under shared directory.
/* <project-root>/src/worker/app-workers/shared/worker-topic.constants.ts */export const WORKER_TOPIC = {
cpuIntensive: 'cupIntensive',
};
  • Create mocked-cpu-intensive.worker.ts under app-workers directory to have logic for CPU intensive processing. Here we are just entering in a while loop for a given duration and returning number of iteration completed.
/* <project-root>/src/worker/app-workers/mocked-cpu-intensive.worker.ts */import { WorkerMessage } from './shared/worker-message.model';export class   MockedCpuIntensiveWorker{public static doWork(value: WorkerMessage): WorkerMessage {
const before = new Date();
let count = 0;
while (true) {
count++;
const now = new Date();
if (now.valueOf() - before.valueOf() > value.data.duration) {
break;
}
}
return new WorkerMessage(value.topic, { iteration: count });
}
}

2. Configure Angular builder to build our web worker code.

  • Create webpack.config.worker.js file under project root directory. This configuration will be used by webpack to build the worker code.
/* <project-root>/webpack.config.worker.js */const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const { NoEmitOnErrorsPlugin } = require('webpack');
const { AngularCompilerPlugin } = require('@ngtools/webpack')
module.exports = {
'mode': 'production',
'devtool': 'none',
'resolve': {
'extensions': [
'.ts',
'.js'
],
'modules': [
'./node_modules'
]
},
'resolveLoader': {
'modules': [
'./node_modules'
]
},
'entry': {
'./src/assets/workers/main': [
'./src/worker/main.worker.ts'
]
},
'output': {
'path': process.cwd(),
'filename': '[name].js'
},
'watch': false,
'module': {
'rules': [
{
'enforce': 'pre',
'test': /\.js$/,
'loader': 'source-map-loader',
'exclude': [
/\/node_modules\//
]
},
{
'test': /\.json$/,
'loader': 'json-loader'
},
{
'test': /\.ts$/,
'loader': '@ngtools/webpack'
}
]
},
'plugins': [
new NoEmitOnErrorsPlugin(),
new ProgressPlugin(),
new AngularCompilerPlugin({
'tsConfigPath': './src/tsconfig.worker.json',
'entryModule': './src/worker/main.worker.ts'
})
]
};
  • Create tsconfig.worker.json under src directory. Important change to notice in this file is the path for outDir and exclusion of main.ts and all .ts files under app folder.
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/worker",
"types": []
},
"exclude": [
"test.ts",
"main.ts",
"**/*.spec.ts",
"**/app/**/*.ts"
]
}
  • Create webpack.config.dev-worker.js under src directory. This configuration will extend base config of worker and will be used for building worker in development mode (by enabling source-map and watch)
/* <project-root>/src/webpack.config.dev-worker.js */const merge = require('webpack-merge');
const baseConfig = require('../webpack.config.worker.js');
module.exports = merge(baseConfig, {
'mode': 'development',
'watch': true
});
  • Update angular.json under project root directory to create builder for worker using @angular-devkit/build-webpack
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"webworker-in-angular6": {
...
"architect": {
...
"build-worker": {
"builder": "@angular-devkit/build-webpack:webpack",
"options": {
"webpackConfig": "./webpack.config.worker.js"
}
},
"dev-worker": {
"builder": "@angular-devkit/build-webpack:webpack",
"options": {
"webpackConfig": "./src/webpack.config.dev-worker.js"
}
},
...
}
},
...
},
"defaultProject": "webworker-in-angular6"
}
  • Update package.json under project root directory to add few convenience script to develop and build worker along with rest of the application. In development mode webpack would be watching for changes for both worker code and application code so in order to launch both of webpack process concurrently, we will use another npm module called concurrently. Install concurrently using command npm install concurrently --save-dev.
{
"name": "webworker-in-angular6",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "concurrently --kill-others \"npm run dev-worker\" \"ng serve\"",
"build": "npm run build-worker && ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build-worker": "ng run webworker-in-angular6:build-worker",
"dev-worker": "ng run webworker-in-angular6:dev-worker"
},
...
}

3. Update application code to use web worker

  • For demonstrating the benefits of web worker, let’s modify the app component created by CLI. Add an animation style for Angular logo to rotate infinitely using browser repaint. This will help us experiencing the freezing behavior of the browser when CPU intensive processing is done in main thread. Create buttons to start calculation in different mode and a placeholder to display the number of iterations completed by mocked CPU intensive logic. Update app.component.html under src/app directory with following code:
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<input type="button" (click)="processInComponent()" value="Process in main UI Thread" />&nbsp;&nbsp;
<input type="button" (click)="processInWorker()" value="Process in Web Worker" />
<br><br>
<label>Iterations: <output>{{iterations}}</output></label>
  • Update app.component.css under src/app with following styles for animation. Note:- The style attribute border-style is added just for this animation to cause browser repaint instead of using GPU.
img {
border: solid 1px orangered;
border-radius: 50%;
-webkit-animation: rotation 2s infinite linear;
}
@-webkit-keyframes rotation {
from {
border-style: solid;
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(359deg);
}
}
  • Create worker.service.ts under src/app. This service will encapsulate the code to initialize and manage web worker.
import { Injectable } from '@angular/core';
import { Subject, Observable, Subscription, fromEvent } from 'rxjs';
import { WorkerMessage } from 'src/worker/app-workers/shared/worker-message.model';@Injectable()
export class WorkerService {
public readonly workerPath = 'assets/workers/main.js';
workerUpdate$: Observable<WorkerMessage>;
private worker: Worker;
private workerSubject: Subject<WorkerMessage>;
private workerMessageSubscription: Subscription;
constructor() {
this.workerInit();
}
doWork(workerMessage: WorkerMessage) {
if (this.worker) {
this.worker.postMessage(workerMessage);
}
}
workerInit(): void {
try {
if (!!this.worker === false) {
this.worker = new Worker(this.workerPath);
this.workerSubject = new Subject<WorkerMessage>();
this.workerUpdate$ = this.workerSubject.asObservable();
this.workerMessageSubscription = fromEvent(this.worker, 'message')
.subscribe((response: MessageEvent) => {
if (this.workerSubject) {
this.workerSubject.next(WorkerMessage.getInstance(response.data));
}
}, (error) => console.error('WORKER ERROR::', error));
}
} catch (exception) {
console.error(exception);
}
}
}

You may notice that in above service we are initializing the worker using a js file assets/workers/main.js that we compiled/transpiled using custom web-pack config webpack.config.worker.js with entry attribute.

  • Modify app.module.ts to provide this newly created service.
...
import { WorkerService } from './worker.service';
@NgModule({
...
providers: [
...
WorkerService
...
],
...
})
export class AppModule { }
  • Update app.component.ts under src/app directory with following code. The component implements handler functions for buttons in template file and use worker service to perform CPU intensive processing in web worker thread.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { WorkerService } from './worker.service';
import { WorkerMessage } from 'src/worker/app-workers/shared/worker-message.model';
import { WORKER_TOPIC } from '../worker/app-workers/shared/worker-topic.constants';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
title = 'webworker-in-angular6';
iterations = 0;
workerTopic = WORKER_TOPIC.cpuIntensive;
workerServiceSubscription: Subscription;
constructor(private workerService: WorkerService) { }ngOnInit() {
this.listenForWorkerResponse();
}
ngOnDestroy(): void {
if (this.workerServiceSubscription) {
this.workerServiceSubscription.unsubscribe();
}
}
processInComponent() {
this.iterations = this.cpuIntensiveCalc(3000).iteration;
}
processInWorker() {
this.iterations = 0;
const workerMessage = new WorkerMessage(this.workerTopic, { duration: 3000 });
this.workerService.doWork(workerMessage);
}
private cpuIntensiveCalc(duration: number) {
const before = new Date();
let count = 0;
while (true) {
count++;
const now = new Date();
if (now.valueOf() - before.valueOf() > duration) {
break;
}
}
return { iteration: count };
}
private listenForWorkerResponse() {
this.workerServiceSubscription = this.workerService.workerUpdate$
.subscribe(data => this.workerResponseParser(data));
}
private workerResponseParser(message: WorkerMessage) {
if (message.topic === this.workerTopic) {
this.iterations = message.data.iteration;
}
}
}

4. We are done with Code changes, Let’s run the application using command npm start and open URL http://localhost:4200

You will see a rotating logo of Angular. Try running CPU Intensive process in Main Thread and Worker Thread to see the impact on responsiveness. Processing in main thread will stop the rotation of the logo.

Finishing words

I hope you enjoyed the article and understood the way to integrate web worker in your Angular 6 application.

Stay tuned for more tips & tricks on how to use web worker in big projects developed using different versions of Angular (4/5).

All source code of the example created in this article is available on GitHub

--

--

Suresh Patidar

Senior Architect at Calsoft. Passionate about UI/UX and Web Development