Run digest cycle in web worker

Dr. Gleb Bahmutov PhD
Kensho Boston / NYC
Personal site http://glebbahmutov.com/
Blog http://glebbahmutov.com/blog/
Twitter @bahmutov

Kensho ♥ Angular

Let us compute primes

Kensho ♥ Angular

function findPrimes(n) {
  var k, primes = [];
  for (k = 0; k < n; k += 1) {
    var prime = findPrime(k + 2);
    primes.push(prime);
  }
  return primes;
}

Kensho ♥ Angular

// page code
var scope = new Scope();
scope.n = 0;

scope.$watch(function watchN(scope) {
  return scope.n;
}, function computePrimes(newVal, oldVal, scope) {
  if (newVal === oldVal) { return; }
  scope.primes = findPrimes(scope.n);
});

document.getElementById('compute').addEventListener('click', 
  function () {
    scope.n += 2000;
    scope.$digest(function (scope) {
      var html = computeMarkup(scope);
      render(html);
    });
  });

Compute!

Kensho ♥ Angular

Coffee scripting

Kensho ♥ Angular

Kensho ♥ Angular

// micro-angular.js
function Scope() {
  this.$$watchers = [];
}
Scope.prototype.$watch = function(watchFn, listenerFn) {
  var watcher = {
    watchFn: watchFn,
    listenerFn: listenerFn || function() { }
  };
  this.$$watchers.push(watcher);
};
Scope.prototype.$digest = function(cb) {
  var self = this;
  var dirty = this.$$watchers.some(function (watch) {
    var newValue = watch.watchFn(self);
    var oldValue = watch.last;
    if (newValue !== oldValue) {
      watch.listenerFn(newValue, oldValue, self);
      watch.last = newValue;
      return true;
    }
  });
  dirty && cb && cb(this);
};

Kensho ♥ Angular

  main context (index.html)  |     worker.js
-----------------------------|---------------------------------------
   loads mock-scopes.js      | loads micro-angular.js and primes.js
   client uses mockScopes    | actual scopes
   loads worker.js           | $digest returns markup text

Move model to a web worker

Digest takes too long?

Kensho ♥ Angular

Kensho ♥ Angular

// index.html app code
var scope = new Scope();
var localN = 0;
scope.set('n', 0);

scope.$watch(function (scope) {
  return scope.n;
}, function (newVal, oldVal, scope) {
  if (newVal === oldVal) { return; }
  scope.primes = findPrimes(scope.n);
});

document.getElementById('compute').addEventListener('click', 
  function () {
    localN += 2000;
    scope.set(n, localN);
    scope.$digest(function (scope) {
      render(computeMarkup(scope));
    });
  });

Kensho ♥ Angular

Web worker micro angular

Kensho ♥ Angular

Client-side mock scopes

function Scope() {
  this.id = '$' + scopes;
  scopes += 1;
  digestWorker.postMessage({
    cmd: 'Scope',
    id: this.id
  });
  console.log('created mock scope', this.id);
}
Scope.prototype.set = function (name, value) {
  digestWorker.postMessage({
    cmd: 'set',
    id: this.id,
    name: name,
    value: value
  });
  console.log('set mock scope', this.id, 'property', name, '=', value);
};
Scope.prototype.$watch = function (watchFn, listenerFn) {
  digestWorker.postMessage({
    cmd: '$watch',
    id: this.id,
    watchFn: watchFn.toString(),
    listenerFn: listenerFn && listenerFn.toString()
  });
};
Scope.prototype.$digest = function ($compile) { ... };

Kensho ♥ Angular

Worker adapter

importScripts('micro-angular.js', 'primes.js');
var scopes = {};
onmessage = function digestOnMessage(e) {
  switch (e.data.cmd) {
    case 'Scope':
      scopes[e.data.id] = new Scope(e.data.id);
    break;
    case 'set':
      scopes[e.data.id][e.data.name] = e.data.value;
    break;
    case '$watch': 
      ...
    break;
    case '$digest':
      scopes[e.data.id].$digest(function digestFinished() {
        var compiled = eval('(' + e.data.$compile + ')');
        var scope = scopes[e.data.id];
        var html = compiled(scope);
        postMessage({
          cmd: 'digestFinished',
          html: html
        });
      });
    break;
  }
};

Kensho ♥ Angular

Could we?

importScripts('micro-angular.js', 'primes.js');
var scopes = {};
onmessage = function digestOnMessage(e) {
  switch (e.data.cmd) {
    case 'Scope':
      scopes[e.data.id] = new Scope(e.data.id);
    break;
    case 'set':
      scopes[e.data.id][e.data.name] = e.data.value;
    break;
    case '$watch': 
      ...
    break;
    case '$digest':
      scopes[e.data.id].$digest(function digestFinished() {
        var compiled = eval('(' + e.data.$compile + ')');
        var scope = scopes[e.data.id];
        var html = compiled(scope);
        var diff = virtualDomDiffs(prevDom, newDom);
        postMessage({
          cmd: 'digestFinished',
          diff: diff
        });
      });
    break;
  }
};

Kensho ♥ Angular

Restore POJO

// BAD
scope.set('n', 0);
// GOOD
scope.n = 0;
// mock scope client-side
function Scope() {
    digestWorker.postMessage({
      cmd: 'Scope',
      id: this.id
    });
    var self = this;
    Object.observe(this, function (changes) {
      changes.forEach(function (change) {
        switch (change.type) {
          case 'add':
          case 'update':
            console.log('change', change.name, 'to', change.object[change.name]);
            self.set(change.name, change.object[change.name]);
          break;
        }
      });
    });
  }

Kensho ♥ Angular

Restored POJO working

Dr. Gleb Bahmutov PhD
Kensho Boston / NYC
Personal site http://glebbahmutov.com/
Blog http://glebbahmutov.com/blog/
Twitter @bahmutov

Kensho ♥ Angular

Run digest cycle in web worker

By Gleb Bahmutov

Run digest cycle in web worker

An experiment in offloading the Angular dirty checking and model update step to a separate browser thread. ng-conf 2015, Salt Lake City, Utah. Video at http://youtu.be/lceLw8ROUP8?list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7

  • 10,422