31

Reading the Angular Documentation, you can find several references to bootstrap your whole Angular app inside a Web Worker, so your UI won't get blocked by heavy JS usage.

However, at this moment there's no official information on how to do that, and the only info in the Angular Doc. is that this is an experimental feature.

How can I use this approach to take advantage of web workers in Angular?

Claies
  • 22,124
  • 4
  • 53
  • 77
Enrique Oriol
  • 1,730
  • 1
  • 13
  • 24

2 Answers2

77

For Angular 7, see answer below.

I spent a lot of time to figure out how to do it, so I hope this can help someone.

Preconditions

I’m assuming that you have an Angular project (version 2 or 4) generated with Angular CLI 1.0 or higher.

It is not mandatory to generate the project with CLI to follow this steps, but the instructions I'll give related with the webpack file, are be based on the CLI webpack config.

Steps

1. Extract webpack file

Since Angular CLI v1.0, there’s the “eject” feature, that allows you to extract the webpack config file and manipulate it as you wish.

  • Run ng eject so Angular CLI generates the webpack.config.js file.

  • Run npm install so the new dependencies generated by CLI are satisfied

2. Install webworker bootstrap dependencies

Run npm install --save @angular/platform-webworker @angular/platform-webworker-dynamic

3. Changes in UI thread bootstrap

3.1 Changes in app.module.ts

Replace BrowserModule by WorkerAppModule in the app.module.ts file. You’ll also need to update the import statement in order to use @angular/platform-webworker library.

//src/app/app.module.ts

import { WorkerAppModule } from '@angular/platform-webworker';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
//...other imports...

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    WorkerAppModule,
    //...other modules...
  ],
  providers: [/*...providers...*/],
  bootstrap: [AppComponent]
})
export class AppModule { }

3.2 Changes in src/main.ts

Replace bootstrap process with: bootstrapWorkerUI (update also the import).

You’ll need to pass a URL with the file where the web worker is defined. Use a file called webworker.bundle.js, don’t worry, we will create this file soon.

//main.ts

import { enableProdMode } from '@angular/core';
import { bootstrapWorkerUi } from '@angular/platform-webworker';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

bootstrapWorkerUi('webworker.bundle.js');

3.3 Create workerLoader.ts file

  • Create a new file src/workerLoader.ts.
  • As your Web Worker will be a single file containing all the required stuff, you need to include polyfills.ts, @angular/core, and @angular/common packages. On next steps, you will update Webpack in order to transpile and build a bundle with the result.
  • Import platformWorkerAppDynamic
  • Import the AppModule (remove the import from main.ts) and bootstrap it using this platformWorkerAppDynamic platform.

// workerLoader.ts

import 'polyfills.ts';
import '@angular/core';
import '@angular/common';

import { platformWorkerAppDynamic } from '@angular/platform-webworker-dynamic';
import { AppModule } from './app/app.module';

platformWorkerAppDynamic().bootstrapModule(AppModule);

4. Update webpack to build your webworker

The webpack auto generated config file is quite long, but you’ll just need to center your attention in the following things:

  • Add a webworkerentry point for our workerLoader.ts file. If you look at the output, you’ll see that it attaches a bundle.js prefix to all chunks. That’s why during bootstrap step we have used webworker.bundle.js

  • Go to HtmlWebpackPlugin and exclude the webworker entry point, so the generated Web Worker file is not included in the index.html file.

  • Go to CommonChunksPlugin, and for the inline common chunk, set the entry chunks explicitely to prevent webworker to be included.

  • Go to AotPlugin and set explicitely the entryModule

// webpack.config.js

//...some stuff...
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CommonsChunkPlugin } = require('webpack').optimize;
const { AotPlugin } = require('@ngtools/webpack');
//...some stuff...

module.exports = {
  //...some stuff...
  "entry": {
    "main": [
      "./src/main.ts"
    ],
    "polyfills": [
      "./src/polyfills.ts"
    ],
    "styles": [
      "./src/styles.css"
    ],
    "webworker": [
      "./src/workerLoader.ts"
    ]
  },
  "output": {
    "path": path.join(process.cwd(), "dist"),
    "filename": "[name].bundle.js",
    "chunkFilename": "[id].chunk.js"
  },
  "module": { /*...a lot of stuff...*/ },
  "plugins": [
    //...some stuff...
    new HtmlWebpackPlugin({
      //...some stuff...
      "excludeChunks": [
        "webworker"
      ],
      //...some more stuff...
    }),
    new BaseHrefWebpackPlugin({}),
    new CommonsChunkPlugin({
      "name": "inline",
      "minChunks": null,
      "chunks": [
        "main",
        "polyfills",
        "styles"
      ]
    }),
    //...some stuff...
    new AotPlugin({
      "mainPath": "main.ts",
      "entryModule": "app/app.module#AppModule",
      //...some stuff...
    })
  ],
  //...some more stuff...
};

You’re ready

If you have followed correctly the previous steps, now you only need to compile the code and try the results.

Run npm start

All the logic of your Angular app should be running inside a WebWorker, causing the UI to be more fluent.

Furter notes

npm start runs the webpack-dev server, and it has some kind of problem with webworkers throwing an error message on console log. Anyway, the webworker seems to run fine. If you compile the app using webpack command and serve it from any http server like simplehttpserver, the error goes away ;)

Sample code and demo

You can get the whole code (webpack config, app.module.ts, ...) from this repo.

You can also watch here a live demo, to check out differences between using Web Workers or not

glemiere
  • 4,790
  • 7
  • 36
  • 60
Enrique Oriol
  • 1,730
  • 1
  • 13
  • 24
  • Great answer @Kaik !! Thanks for helping the community about all that :) Out of curiosity, have you notice a perf improvement ? – maxime1992 Apr 14 '17 at 08:51
  • @Maxime you can try the demo in the link. There's a huge benefit in terms of user experience, as UI works smoothly even if you are running heavy stuff inside the business logic. – Enrique Oriol Apr 15 '17 at 07:03
  • I didn't saw the demo link earlier thanks for pointing that out ! It's incredible ... Can't wait to try that on my app ! Thanks again :) – maxime1992 Apr 15 '17 at 18:36
  • @KaiK great answer! I just tried running this on a fresh install of cli v1.0 and when i run npm start the page just hangs on Loading... – Jack Clancy Apr 21 '17 at 19:38
  • @JackClancy is the console showing you any error? You can download the code from the repo and check if there's some difference with your project – Enrique Oriol Apr 21 '17 at 20:16
  • Great job, did you get a change to run Angular Material 2 with Web Worker and complete Ionic 2 App in Web Worker? – Naveed Ahmed May 04 '17 at 17:18
  • Good answer, but, for more complex projects there's a whole host of issues with postcss, module loading and associated webpack nasties to sort out. Including the fact that webworker.js seems to load twice if you need to hash your output files. I've not sorted them all out yet, but I'll post a round-up if I do. – PeterS Jun 07 '17 at 16:50
  • @Ced It really is very tricky. I was getting a whole host of errors (mostly postcss stuff) and it is a huge time drain. I have hived the project off from my main code base and return when time permits.I still haven't found the reason that webworker.js is loading twice. One is in includes and the other is via generated code. I'm going to have another go this weekend. – PeterS Jun 21 '17 at 08:34
  • @EnriqueOriol - thanks for the detailed answer. I am using web-animations and getting the error `web-animations.min.js:15 Uncaught ReferenceError: document is not defined` when trying to load the page. Do you think you can help me out there ? Thanks in advance. – belafarinrod91 Aug 03 '17 at 09:46
  • @belafarinrod91 probably web-animations is using unsafe DOM APIs, like "document", and because you cannot access the DOM in webworkers, you're getting this error – Enrique Oriol Aug 03 '17 at 09:49
  • @EnriqueOriol so there is no solution yet ? – belafarinrod91 Aug 03 '17 at 11:26
  • @belafarinrod91 Angular provides safe DOM API for accessing and altering the DOM in order to be able to do that stuff and run the code in multiple platforms (like Webworkers). The problem here is not Angular, but the library "web-animations" you're trying to include. That is what causes the error, the same way your app will throw an error if you try to provide through server side rendering, because "document" is neither present in NodeJS. You should remove that library and do whatever animations you want to do using CSS or Angular. – Enrique Oriol Aug 03 '17 at 13:52
  • @EnriqueOriol thanks for your explanations, but I need to use the `web-animations` polyfill to ensure that the angular animations are executed in IE and FF ... so probably there is currently no solution available to fulfill my requirements. Thanks anyways! – belafarinrod91 Aug 14 '17 at 05:16
  • Thanks for the answer, I am using `material2` and after using this solution I get an error `hammer.js:2643 Uncaught ReferenceError: window is not defined`. It seems that Angular should get rid of all 3rd party libraries that does not use the safe DOM API – Murhaf Sousli Sep 08 '17 at 06:09
  • 2
    @MurhafSousli If you are using material2 it is not possible to use webworker as it makes many calls to manipulate the DOM. Currently material2 and webworkers are a non-starter. – PeterS Nov 09 '17 at 17:31
  • I had follow the same steps but got following errors ERROR in ./src/main.ts Module build failed: F:\~\angular-cli-master\angular-cli-master\packages\@ngtools\webpack\src\index.ts:2 import { satisfies } from 'semver';ERROR in ./src/polyfills.ts Module build failed: F:\~\angular-cli-master\angular-cli-master\packages\@ngtools\webpack\src\index.ts:2 import { satisfies } from 'semver'; – Mantu Nigam Dec 05 '17 at 15:04
  • @EnriqueOriol: thx for your nice guide above - do you know why `(dragstart)` doesn't work in templates? I included this approach https://www.radzen.com/blog/angular-drag-and-drop/, but it doesn't work. I thought it's connected to the `@HostListener` but even `(dragstart)` didn't work. `(click)` event worked right away. `onDragstart="..."`worked as well. – Trinimon Apr 08 '18 at 13:45
  • No idea why `(dragstart)` is not working and `onDragstart` works fine. Don't know if it is related, but remember Web workers do not have access to DOM. – Enrique Oriol Apr 09 '18 at 14:59
  • @Enrique, I am getting error when running 'npm start' - module.js:557 throw err; ^ Error: Cannot find module '@ngtools/webpack'. How to fix esissue, Can you suggest something? – Mahi Jul 18 '18 at 09:00
  • @Ahmadmnzr as you can see in the "Further notes" section, `npm start` runs the webpack-dev server, and it has some kind of problem with webworkers throwing an error message on console log, but the webworker seems to run fine.If you compile the app using webpack command and serve it from any http server like simplehttpserver, the error goes away ;) – Enrique Oriol Jul 18 '18 at 09:17
  • Thanks,ok, but how to compile the app using webpack command and serve it. – Mahi Jul 18 '18 at 09:21
  • I get `Error: webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.` – hoefling Sep 20 '18 at 12:51
  • Thanks for this article. `ng eject` has been disabled in `Angular 7.0.1`.. how to move further? – Mr_Green Oct 31 '18 at 09:01
  • This post needs to be updated to angular 7. By the way, animations doesn't work with platform webworker – Serginho Jan 23 '19 at 10:51
  • A bit late to the party but I'd like to see how I can do that with Angular 12, especially since `eject` is no longer an option, plus they switched to Webpack 5... – AsGoodAsItGets Nov 26 '21 at 16:25
2

Good news guys, I got this to work with Angular 7!

Requirement: npm install --save-dev @angular-builders/custom-webpack html-webpack-plugin Make sure that you have production:true in your env file if you just want to copy/past code from below.

Step 1: Edit your angular.json file the following way:

"architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
                "path": "./webpack.client.config.js",
                "replaceDuplicatePlugins": true
             },
            ...
          }

You are only editing the build part because you don't really need the whole worker thing in dev server.

Step 2: Create webpack.client.config.js file at the root of your project. If you're not using SSR, you can remove exclude: ['./server.ts'],

const path = require('path');
const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { AngularCompilerPlugin } = require('@ngtools/webpack');

module.exports = {
  entry: {
    webworker: [
      "./src/workerLoader.ts"
    ],
    main: [
      "./src/main.ts"
    ],
    polyfills: [
      "./src/polyfills.ts"
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      excludeChunks: [
        "webworker"
      ],
      chunksSortMode: "none"
    }),
    new AngularCompilerPlugin({
      mainPath: "./src/main.ts",
      entryModule: './src/app/app.module#AppModule',
      tsConfigPath: "src/tsconfig.app.json",
      exclude: ['./server.ts'],
      sourceMap: true,
      platform: 0
    }),
  ],
  optimization: {
    splitChunks: {
      name: "inline"
    }
  }
}

Step 3: Edit you AppModule:

import { BrowserModule } from '@angular/platform-browser'
import { WorkerAppModule } from '@angular/platform-webworker'
const AppBootstrap =
            environment.production
            ? WorkerAppModule
            : BrowserModule.withServerTransition({ appId: 'myApp' })
    imports: [
        ...
        AppBootstrap,
        ...
    ]

Step 4: Edit you main.ts file.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { bootstrapWorkerUi } from '@angular/platform-webworker';

import {AppModule} from './app/app.module';
import {environment} from './environments/environment';

if (environment.production) {
  enableProdMode();
  bootstrapWorkerUi('webworker.bundle.js');
}

else {
  document.addEventListener('DOMContentLoaded', () => {
    platformBrowserDynamic().bootstrapModule(AppModule);
  });
}

Step 5: It will compile just fine, but you may have a runtime issue due to DOM manipulation in your app. At this point you just have to remove any DOM manipulation and replace it by something else. I'm still working at figuring this part out and will edit my answer later to give direction about this issue.

If you're not doing savage DOM manipulation, then you're good to go with a free main-thread and auditing your app using lighthouse should not show Minimize main-thread work anymore, as most of your app except UI loads in a second thread.

glemiere
  • 4,790
  • 7
  • 36
  • 60
  • The issue is like: Cannot find window in a line like window['webpackJSONPCallback']??? You got nothing. You have to use webpack multiple targets to compile it, but angular cli don't support an array in the webpack config. Lazy loading won't work for this reason. – Serginho Feb 27 '19 at 07:10
  • Really ? I don't have this issue on my end even though I use lazy loading. – glemiere Feb 27 '19 at 14:39
  • So how do you load lazy modules with webpack configured with target: 'web'? You have to get an error: cannot find window. Isn't it? If not, show me an example. I'm absolutely sure this won't work. See this: https://webpack.js.org/concepts/targets/ – Serginho Feb 27 '19 at 15:15