Web Worker in Angular

4 분 소요

Leveraging Workers in Angular Applications

Workers provide a mechanism to execute JavaScript code in background threads, thereby preventing blocking of the main thread and enhancing performance in Angular applications. This article explores the creation and utilization of workers within an Angular context.

1. Generating a Worker

For convenience, the Angular CLI provides a command to automatically generate worker files. Particularly, when utilizing Nx, it’s highly recommended to use ng generate or ng g, as this automatically creates the tsconfig.webworker.ts file.

ng generate web-worker [path]
# example: ng generate web-worker core/workers/calculation

Note, however, that the auto-generated code directly accesses the worker from a component, differing from the instance-based approach described below. To align with the demonstrated code, some modification of the generated code is necessary.

Refer to “Manually Configuring Workers in an Nx Environment” at the end of this document for detailed instructions.

2. Crafting a Worker File

Begin by creating a dedicated worker file within your Angular project, typically using the .worker.ts extension. Here’s a basic example:

// calculation.worker.ts
addEventListener('message', ({ data }) => {
  console.log('Worker: message received->', data);
  
  // complex calculation in background
  const result = data * data;

  // return result to main thread
  postMessage(result);
});

3. Developing a Worker Service

Next, create an Angular service to manage the worker.

Typically, Angular services utilize @Injectable for dependency injection. However, to prevent worker sharing issues when the service is reused across components, it’s advisable to instantiate a separate Worker instance for each component instead of injecting the service.

// worker.service.ts
import { signal, WritableSignal } from '@angular/core';

export class WorkerService {
  private worker: Worker;

  // state management
  public readonly result: WritableSignal<number | undefined> = signal(undefined);
  public readonly isLoading: WritableSignal<boolean> = signal(false);
  public readonly error: WritableSignal<any | undefined> = signal(undefined);

  constructor(workerPath: string) {
    this.worker = new Worker(new URL(workerPath, import.meta.url), { type: 'module' });

    this.worker.onmessage = ({ data }) => {
      this.result.set(data);
      this.isLoading.set(false);
    };

    this.worker.onerror = (err) => {
      this.error.set(err);
      this.isLoading.set(false);
      console.error('Worker error:', err);
    };
  }

  runWorker(input: number): void {
    // initialize
    this.result.set(undefined);
    this.error.set(undefined);
    this.isLoading.set(true);
    
    // send start message to the Worker
    this.worker.postMessage(input);
  }

  // terminate worker when component is removed
  terminate(): void {
    this.worker.terminate();
    console.log('Worker is terminated.');
  }
}

4. Integrating the Worker in a Component

Finally, utilize the service created above within an Angular component to interact with the web worker.

// app.component.ts
import { Component, OnDestroy } from '@angular/core';
import { WorkerService } from './worker.service';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h2>Angular Worker with Signals</h2>
    <input #inputField type="number" value="10" />
    <button (click)="runWorker(inputField.valueAsNumber)">Run Worker</button>

    @if (worker.isLoading()) {
      <p>processing...</p>
    }

    @if (worker.result(); as result) {
      <p class="result">Result: </p>
    }

    @if (worker.error(); as error) {
      <p class="error">Error: </p>
    }
  `,
  styles: [`
    .result { color: green; font-weight: bold; }
    .error { color: red; }
  `]
})
export class AppComponent implements OnDestroy {
  readonly worker = new WorkerService('./calculation.worker.ts');
  
  constructor() {}

  runWorker(value: number) {
    if (isNaN(value)) {
      alert('inter any valid number.');
      return;
    }
    this.worker.runWorker(value);
  }

  ngOnDestroy(): void {
    this.worker.terminate();
  }
}

Conclusion

You can now offload tasks to background threads using workers and transmit the results back to the main thread, leading to improved application responsiveness and overall performance.

Optional: Manually Configuring Workers in an Nx Environment

Manually Creating a tsconfig for Workers

Create a tsconfig.worker.json file in the same directory as your tsconfig.json file and insert the following code:

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/worker",
    "lib": [
      "es2018",
      "webworker"
    ],
    "types": []
  },
  "include": [
    "src/**/*.worker.ts"
  ]
}

Registering tsconfig.webworker.json in angular.json

You must register the tsconfig.webworker.json file created above in the angular.json file. When generated automatically, it is typically added to two locations:

  • architect > build > options
  • test > options

The value to add is:

"webWorkerTsConfig": "tsconfig.worker.json"

The complete angular.json code after automatic generation is as follows:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "webworker": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/webworker",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": [
              "zone.js"
            ],
            "tsConfig": "tsconfig.app.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "webWorkerTsConfig": "tsconfig.worker.json"
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "buildOptimizer": false,
              "optimization": false,
              "vendorChunk": true,
              "extractLicenses": false,
              "sourceMap": true,
              "namedChunks": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "bro
wserTarget": "webworker:build:production"
            },
            "development": {
              "browserTarget": "webworker:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "webworker:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "webWorkerTsConfig": "tsconfig.worker.json"
          }
        }
      }
    }
  }
}

댓글남기기