️technology

How To Use Web Workers in an Angular Application

The Web Workers API is a feature of modern browsers which enables running scripts in a background thread separate from the browser’s main thread, keeping it free to focus on updating your web app’s UI and making your application smooth and responsive.

Web Workers are now supported in all major browsers except Opera Mini, which is mostly used on feature phones with limited processing power.

How to Add a Web Worker in an Angular Component

Adding a web worker in Angular is surprisingly easy – just run one Angular CLI command:

ng g web-worker <location>

With this, it will generate a *.worker.ts file, scaffold the minimal usage in the location you indicate, and configure the app for using web workers.

The location can be any Angular component.

So, for example, if you want to generate a worker for the root App component, just run the following in the terminal.

ng g web-worker app

This will generate an app.worker.ts file beside app.component.ts

Demo Angular App with Web Workers

In this next section let’s create a basic demo angular application that sorts a list of super hero names – just to make it familiar if you have ever followed Angular’s tour of heroes tutorial.

This emoji 👨🏻‍💻 means you are going to write some code!

First, let’s create a new app web-worker-demo with the defaults (i.e., no routing and using CSS for styling).

ng new web-worker-demo --defaults

This creates your demo app in a directory named web-worker-demo and installs all node module dependencies.

Once done, open the generated directory and run the angular development server.

cd web-worker-demo
ng serve

If you open your browser to localhost:4200 (or whatever the dev server shows you), you should find a default initial Angular app running which should look like this:

default-ng-app

Great! Now that we have an Angular app, lets display some super hero names.

👨🏻‍💻 Open the App component class file app.component.ts and replace all of its contents with the following:

// app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'Awesome Heroes';
  heroes = [
    { id: 12, name: 'Dr. Nice' },
    { id: 15, name: 'Magneta' },
    { id: 13, name: 'Bombasto' },
    { id: 17, name: 'Dynama' },
    { id: 16, name: 'RubberMan' },
    { id: 14, name: 'Celeritas' },
    { id: 19, name: 'Magma' },
    { id: 18, name: 'Dr. IQ' },
    { id: 20, name: 'Tornado' },
  ];
}

👨🏻‍💻 Then replace everything in the App component template app.component.html with:

<!-- app.component.html -->

<h1>{{ title }}</h1>

<ul class="hero-list">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{ hero.id }}</span>
    <span class="name">{{ hero.name }}</span>
  </li>
</ul>

<button>Sort by ID</button>
<button>Sort by Name</button>



👨🏻‍💻 Let’s make it look a bit fancy with by putting the following in the App component CSS app.component.css:

h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 200%;
}

button {
  background-color: #eee;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  color: black;
  font-size: 1.2rem;
  padding: 1rem;
  margin: 1rem 1rem 1rem 0;
}

button:hover {
  background-color: black;
  color: white;
}

ul.hero-list {
  list-style: none;
}

ul.hero-list li {
  border: 1px solid #369;
  border-radius: 5px;
  width: 200px;
  margin-bottom: 15px;
}

span.badge {
  color: white;
  background-color: #369;
  padding: 0 1rem;
  margin-right: 1rem;
}

Your app should look like this:

Hero List App

Notice the list is not sorted at all – neither by ID or Name. Let’s fix that.

Now, it’s time to create a web worker.

In your terminal, run the command to add a web worker for the App component:

ng g web-worker app

After running this, a new app.worker.ts file is generated, and your app.component.ts file will be updated with a minimal usage.

Go on, check around what has changed in your app’s code.

Do you see the generated code in the App component file? We are going to use this later.

If your browser is now showing a blank white page, unfortunately, this is because the angular configuration was updated.

Don’t panic. You just have to restart (i.e., stop and start) your development server.

Most operating system terminals allow this by pressing [CTRL]+’C’ and running ng serve again.

👨🏻‍💻 Open app.component.ts, create a sortHeroes method that accepts a flag string as argument in the AppComponent class.

👨🏻‍💻 Next, move all the generated code for using the web worker in it, and you should have this:

// app.component.ts

  sortHeroes(flag: string) {
    if (typeof Worker !== 'undefined') {
      // Create a new
      const worker = new Worker(new URL('./app.worker', import.meta.url));
      worker.onmessage = ({ data }) => {
        console.log(`page got message: ${data}`);
      };
      worker.postMessage('hello');
    } else {
      // Web Workers are not supported in this environment.
      // You should add a fallback so that your program still executes correctly.
    }
  }

In this minimal usage generated by Angular CLI, we see that it has an if-else condition that checks if a Worker class exists in the environment. This allows us to write a fallback logic inside the else block for when the browser/environment does not support Web Workers.

If the Worker class exists (i.e., its type is not undefined), this will create a new worker instance.

The worker instance can accept a callback for its onmessage property, and allow the creator (i.e., our AppComponent) to send it messages via its postMessage method.

More on the usage of these below.

👨🏻‍💻 Now in the App template app.component.html let’s update the <button> elements so that they will call our new sortHeroes method every time the user clicks our button and pass the flag.

<!-- app.component.html -->

<button (click)="sortHeroes('id')">Sort by ID</button>
<button (click)="sortHeroes('name')">Sort by Name</button>

Check your devtools console and try clicking the buttons to see what’s happening.

Devtools Console

If you see this, your app is now creating and using a web worker.

👨🏻‍💻 Now update the sortHeroes method so that it will call worker.postMessage with an object containing heroes and the received flag argument.

👨🏻‍💻 Also, update the worker.onmessage callback so that it will assign the processed data to this.heroes.

Your sortHeroes method should now be:

// app.component.ts

  sortHeroes(flag: string) {
    if (typeof Worker !== 'undefined') {
      // Create a new
      const worker = new Worker(new URL('./app.worker', import.meta.url));
      worker.onmessage = ({ data }) => {
        this.heroes = data;
      };

      worker.postMessage({ heroes: this.heroes, flag });
    } else {
      // Web Workers are not supported in this environment.
      // You should add a fallback so that your program still executes correctly.
    }
  }

This sends a message to our worker, letting it know that we want to sort the heroes list using the flag property.

After the worker instance is done processing, it will then send a message back to its creator and this will trigger the onmessage callback where we assign the processed data back to our heroes list.

👨🏻‍💻 Now let’s program our worker to do the sorting. Open app.worker.ts and replace its contents with the following:

// app.worker.ts

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

addEventListener('message', ({ data }) => {
  const { heroes, flag } = data;
  const response = heroes.sort((a: any, b: any) => {
    if (a[flag] < b[flag]) return -1;
    if (a[flag] > b[flag]) return 1;
    return 0;
  });
  postMessage(response);
});

What’s happening here? First, a callback is attached to the message event which will be triggered when the message sent by AppComponent is received.

Next, the data object received will contain the heroes array and flag in it, so we destructure the received data and use this information for the heroes.sort callback.

Now check your app again in your browser and click around the two buttons.

The heroes list should now be sorted depending on which button you click.

Sorted by ID Sorted by Name
sorted by id sorted by name

Congratulations! You just built an Angular app that sorts a list of heroes by name or ID… using a Web Worker! 🎉

The code for this demo app is in my GitHub repository, and a live demo is deployed on my website.

Gochas in Using Web Workers in Angular

In our demo app, we only handled the scenario when Web Workers are supported in the browser. You should definitely always handle the other scenario so that it will still work even without web workers.

In our example above, just implement the sorting logic we did from app.workers.ts in the else block inside your sortHeroes method.

I would advice to have a function in a separate file (e.g., sort-heroes.ts) for this so that you can just import the function to be used in both app.component.ts and app.worker.ts

One other gocha that I encountered in my work with Web Workers in Angular, is that Jest (our unit test runner) needed to be configured to support EcmaScript Modules (ESM). The dynamic import import.meta.url will produce an error in your unit test if your runner does not support ESM.

In Summary…

We now know how to use Web Workers in Angular. We have seen how easy it is to use Angular CLI to add a worker for any component in our app. We even built a simple demo application that creates and sends a message to a web worker so that it will sort an array for us.

Thank you for reading!

If you find this helpful or have ideas to improve it, I always appreciate feedback. You can send me an email ayo[at]ayco.io or follow me on Mastodon @ayo@ayco.io

Thoughts?

For more technology posts like this, subscribe to my RSS feed