One of the most compelling introductions in the new rendition of the Angular framework is the total abstraction of the rendering execution. This stems from one of the core ideas of Angular: you should be able to seamlessly substitute out any behavior module and replace it with another. This, of course, means that Angular cannot have any dependency bleed outside of the modules.
One place that Angular puts emphasis on being configurable is the location where code execution takes place. This is manifested in a number of ways, and this recipe will focus on Angular's ability to perform rendering execution at a location other than inside the main browser's JavaScript runtime.
The code, links, and a live example related to this recipe are available at .
Begin with some simple application elements that do not yet form a full application:
[index.html] <!DOCTYPE html> <html> <head> <script src="zone.js "></script> <script src="reflect-metadata.js"></script> <script src="system.src.js"></script> <script src="system-config.js"></script> </head> <body> <article></article> <script> System.import('system-config.js') .then(function() { System.import('main.ts'); }); </script> </body> </html> [app/article.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'article', template: ` <h2>{{title}}</h2> ` }) export class ArticleComponent { title:string = 'Survey Indicates Plastic Funnel Best Way to Drink Rare Wine'; }
Note that this recipe uses SystemJS to handle modules and TypeScript transpilation, but this is merely to keep the demonstration simple. A properly compiled application can use regular JavaScript to accomplish the same feat, with no dependency on SystemJS.
This recipe will start off by assuming you have a basic knowledge of what web workers are and how they work. There is a discussion of their properties later in this recipe.
The SystemJS startup configuration kicks off the application from main.ts
, so you'll begin there. Instead of bootstrapping the application in this file as you normally would, you'll use an imported Angular helper to initialize the web worker instance:
[main.ts] import {bootstrapWorkerUi} from "@angular/platform-webworker"; bootstrapWorkerUi(window.location.href + "loader.js");
Concern yourself with the precise details of what this is doing later, but the high-level idea is that this is creating a web worker instance that is integrated with the Angular Renderer.
Recall that initializing a web worker requires a path to its startup JS file, and a relative path inside this file might not always work; therefore, you're using the window location to provide an absolute URL. The necessity of this may differ based on your development setup.
Since this file references a web worker initialization file called loader.js
, write it next. Workers cannot be given a TypeScript file:
[loader.js] importScripts( "system.js", "zone.js", "reflect-metadata.js", "./system-config.js"); System.import("./web-worker-main.ts");
This file first imports the same files that are listed inside the <head>
tag of index.html
. This should make sense since the web worker will need to perform some of the duties of Angular but without direct access to anything that exists inside the main JavaScript runtime. For example, since this web worker will be rendering a component, it needs to be able to understand the @Component({})
notation, which cannot be done without the reflect-metadata extension.
Just like the main application, the web worker also has an initialization file. This takes the form of web-worker-main.ts
:
[web-worker-main.ts] import {AppModule} from './app/app.module'; import {platformWorkerAppDynamic} from '@angular/platform-webworker-dynamic'; platformWorkerAppDynamic().bootstrapModule(AppModule);
Compared to a normal main.ts
file, this file should look delightfully familiar. Angular provides you with a totally separate platform module, but one that affords you an identical API, which you are using here to bootstrap the application from the yet-to-be-defined AppModule
. Define this next:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {WorkerAppModule} from '@angular/platform-webworker'; import {AppModule} from './app.module'; import {ArticleComponent} from './article.component'; @NgModule({ imports: [ WorkerAppModule ], bootstrap: [ ArticleComponent ], declarations: [ ArticleComponent ] }) export class AppModule {}
Similar to how you would normally use BrowserModule
within a conventional top-level app module, Angular provides a web-worker-flavored WorkerAppModule
that handles all the necessary integration.
Make no mistake about what is happening here: your application is now running on two separate JavaScript threads.
Web workers are basically just really dumb JavaScript execution buckets. When initialized, they are given an initial piece of JavaScript to run, which in this example took the form of loader.js/
. They have no understanding of what is going on in the main browser runtime. They can't interact with the DOM, and you are only able to communicate with them via PostMessages. Angular builds an elegant abstraction on top of PostMessages to create a bus interface, and it is this interface that is used to join the two runtimes together.
If you look into the PostMessage specification, you will notice that all of the data passed as the message must be serialized into a string. How then can this rendering configuration possibly work with the DOM in the main browser, handling events and displaying the HTML and the web worker performing the rendering on a DOM it cannot touch?
The answer is simple: Angular serializes everything. When the initial rendering occurs, or an event in the browser occurs, Angular grabs everything it needs to know about the current state, wraps it up into a serialized string, and ships it off to the web worker renderer on the proper channel. The web worker renderer understands what's being passed to it. Although it cannot access the main DOM, it is certainly able to construct HTML elements, understand how the serialized events passed to it will affect them, and perform the rendering.
When the time comes to tell the browser what to actually render, it will in turn serialize the rendered component and send it back to the main browser runtime, which will unpack the string and insert it into the DOM.
To the Angular framework, because it is abstracted from all the browser dependencies that might get in the way of this, everything seems normal. Events come in, they're handled, and a renderer service tells it what to put into the DOM. Everything that happens in between is unimportant to the Angular framework, which doesn't care that everything happened in a totally separate JavaScript runtime.
Note that the elements you begin with have nothing unusual about them to allow web worker compatibility. This underscores the elegance of Angular's web worker abstraction.
As web workers are more fully supported and utilized, patterns such as these will most likely become more and more common, and it is extremely prescient of the Angular team to support this behavior. There are, however, some considerations.
One of the primary benefits of using web workers is that you now have access to an execution context that is not blocked by anything running on the browser execution context. For performance drags, such as reflow and blocking of event loop handlers, the web worker will continue with the execution without a care for what is happening elsewhere.
Therefore, getting a performance benefit becomes a problem of optimization. Communication between the two JavaScript threads requires serialization and transmission of events, which is obviously not as fast as handling them in the same thread. However, in an especially complicated application, rendering can quickly become one of the most expensive things your application will do. Therefore, you may need to experiment and make a judgment call for your application, as not all applications will see performance gains from using web workers—only those where rendering becomes prohibitively expensive.
Web workers have extremely good support, but there is still a very significant number of browsers that do not support them. If your application needs to work universally, web workers are not recommended; since they will not gracefully degrade, your application will merely fail.