Run Angular in Web Worker

Load and run full AngularJS 1.x in browser's separate thread.

demo, demo code

Recently I have shown how to load and render a simple AngularJS application in a synthetic browser environment under NodeJS. One can use this technique to fully test the application's code using a clean environment for each unit test. At ng-conf 2015 I showed how to run a simple implementation of Angular's dirty checking and digest cycle inside a web worker.

In this blog post I will show how to load and run the full standard Angular 1.x inside a browser's Web Worker. This instance of Angular will run separately from the main browser's window (where the original AngularJS can be running or not), will communicate with the main window via postMessage API, and will not block the browser's responses while processing data.

Before proceeding, let me state the differences in the three environments where I got AngularJS running: browser, NodeJS and Web Worker. This will be helpful when trying to make Angular work inside a web worker.

browser           |        NodeJS        |    NodeJS + jsdom (benv)   |   Web Worker
------------------------------------------------------------------------------------------
window            |  global              |    global, window          |     self
document          |  -                   |    document                |       -
-                 |  module, require     |    module, require         |       -
window.location   |  __filename          |    __filename              |       -

Compared to the browser environment, or even plain NodeJS runtime, the Web Worker runtime is very bare bones. AngularJS requires both window and document objects to run (the window object to do everything, and the document object to mainly run document.getElementById and document.attachEventListener methods).

Thus I have decided to do the following:

Step 1 - load Angular under Node

I created the synthetic environment using jsdom via benv wrappers. These libraries create both window and document objects that can fool any other library.

NodeJS + jsdom (benv) environment
1
2
3
4
5
6
var benv = require('benv');
benv.setup(function () {
console.log('have window?', typeof window !== 'undefined');
console.log('have document?', typeof document !== 'undefined');
console.log('created window and document');
});

Creating a mock browser environment is pretty simple, but how to load the Angular framework into the mock window? Well, inside the setup callback we do have a window and a document, so we can just load angular.js as if it were a CommonJS module.

test.js NodeJS + jsdom (benv) environment
1
2
3
4
5
6
7
8
var benv = require('benv');
benv.setup(function () {
console.log('have window?', typeof window !== 'undefined');
console.log('have document?', typeof document !== 'undefined');
console.log('created window and document');
require('./bower_components/angular/angular.js');
console.log('loaded angular', window.angular.version);
});

When run, this code displays

have window? true
have document? true
created window and document
loaded angular { full: '1.2.25',
  major: 1,
  minor: 2,
  dot: 25,
  codeName: 'hypnotic-gesticulation' }

Nice!

Step 2 - pack everything from Node to run in the browser

Wait, we need to pack the synthetic objects that mimic the browser environment to run inside the browser?

Yes.

We have loaded the Angular library in a synthetic environment running inside the Node runtime. Most parts of the code live in the CommonJS modules, like node_modules/benv/index.js and its dependencies. We need to pack everything together and create a bundle we can load inside the browser. Luckily there is a tool for that: browserify. We will create a stand alone bundle from the code above

$ browserify test.js -o test-bundle.js

The test-bundle.js will be huge - it will have all the code from out test.js plus benv, plus its dependencies (like jsdom), plus some parts of the Node system libraries.

Step 3 - remove parts that assume the browser environment

We cannot load the test-bundle.js right away, unfortunately. While browserify command packed everything and added shims to recreate the Node environment, some code inside jsdom and its dependencies assumed Node. For example, there are places where __filename variable was used directly. Remember, browserify only tried to bundle up our application that created a synthetic window for NodeJS environment.

A second obstacle to loading the test-bundle.js in the browser was: we are NOT loading the bundle in the browser environment (the first column of the "environments" table). Instead we are going to load this bundle in the web worker column. Browserify assumes that it has a window object when shimming the NodeJS system libraries.

Luckily these parts were few and could be easily removed. For example, there were places where the CommonJS require call was patched over. There will be no need for require in the web worker, thus I could easily comment these out. My demo application also does not perform any Ajax calls, thus I could comment out the xhrHttp code that checked for the window object's presence. Again. the window and document will only be created inside the benv.setup(...) callback. These objects cannot be used anywhere before the benv.setup call executes.

Step 4 - bootstrap Angular application and return digested HTML

The last step to show the AngularJS framework working inside a web worker is to bootstrap an angular application and run a digest cycle. Here is a small application I placed inside the benv.setup() callback. The synthetic document got its HTML inside the benv.setup callback too.

angular application inside benv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
benv.setup(function () {
var html = '<h1 ng-controller="helloController">Hello {{ title }}</h1>';
document.body.innerHTML = html;
(function setupApp(angular) {
angular.module('myApp', [])
.controller('helloController', ['$scope', '$timeout', function ($scope, $timeout) {
$timeout(function setTitle() {
$scope.title = 'from Angular ' + angular.version.full + ' running in Web Worker!';
$timeout(function afterDigest() {
// communicate back to the page
self.postMessage(document.body.innerHTML);
});
}, 1000);
}]);
}(window.angular));
(function startApp(angular) {
angular.bootstrap(document.body, ['myApp']);
}(window.angular));
});

For clarity I wrapped the application definition in the setupApp function, and its bootstrapping in the startApp function. The application has a single controller with a single two-way bound expression. I am using two classical Angular services $scope and $timeout. After the first 1 second time out finishes, the scope property title changes. Then I let the digest run by setting another time out. The digest cycle finds the changed scope property and updates the HTML inside the synthetic document.body.

The callback afterDigest runs AFTER the DOM update and grabs the rendered HTML and sends it back to the main browser window using self.postMessage. The demo page listens for a message and sticks the received HTML directly into the output element.

browser page
1
2
3
4
5
6
7
<div id="output">rendered HTML from web worker will replace this text.</div>
<script>
var worker = new Worker('angular-web-worker.js');
worker.onmessage = function (e) {
document.getElementById('output').innerHTML = e.data;
};
</script>

That is it. Seems like the digest cycle is running, and at least some basic AngularJS services and directives are working: $scope, $timeout and ng-controller. In the future I plan to simplify and separate the application from the wrapped framework bundle to allow any arbitrary application to execute in a web worker context.

Conclusion

I hope to use the separate Web Worker threads each running an AngularJS framework to speed up certain tasks, like loading the initial application code heavy page.