Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular CLI generated app with Web Workers

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?

like image 974
Enrique Oriol Avatar asked Apr 07 '17 10:04

Enrique Oriol


People also ask

Does angular use web workers?

To add a web worker to an existing project, use the Angular CLI ng generate command. You can add a web worker anywhere in your application. For example, to add a web worker to the root component, src/app/app.

What are the limitations of web workers in angular?

Web Worker Limitations & PitfallsFunctions and methods cannot be passed to Web Workers. When an object is passed to a Web Worker, all of its methods are removed. If a function is passed to a web worker, the following exception will occur.

How many web workers can run concurrently?

A web worker is a JavaScript program running on a different thread, in parallel with main thread. The browser creates one thread per tab. The main thread can spawn an unlimited number of web workers, until the user's system resources are fully consumed.

What is the difference between service worker and web worker?

Unlike web workers, service workers allow you to intercept network requests (via the fetch event) and to listen for Push API events in the background (via the push event). A page can spawn multiple web workers, but a single service worker controls all the active tabs under the scope it was registered with.


2 Answers

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

like image 149
Enrique Oriol Avatar answered Sep 26 '22 06:09

Enrique Oriol


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.

like image 23
Dev Avatar answered Sep 23 '22 06:09

Dev