17.03.2014, codeshow

  • 40 million users
  • learn 12 languages

  • 1 web app (AngularJS)
  • 2 mobile apps (iOS & Android)
  • 1 backend


Beta version: http://busuu.com/candidates

AngularJS app

  • > 121 controllers
  • > 150 services
  • 177 HTML templates
  • 67 directives
  • > 15 grunt tasks

Challenges:

  • Maintainability
  • Performance

Beta version: http://busuu.com/candidates

Why Busuu <3 AngularJS?

elegant data-binding (Plain Old Javascript Objects)

// Backbone
var Person = Backbone.Model.extend();
var person = new Person();
person.set({name: 'Nick'});
console.log(person.get('name'));

// AngularJS
$scope.person = new Person();
$scope.person.name = 'Nick';
console.log(person.name);
  • straightforward
  • readability
  • models can use inheritance

Why Busuu <3 AngularJS?

directives (extend HTML)

  • abstract DOM manipulation
  • keep DOM manipulation out of controllers & services

Why Busuu <3 AngularJS?

testability (dependency injection)

// LearningUnit_test.js
beforeEach(module(function($provide) {
  $provide.value('berryService', jasmine.createSpyObj('berryService', ['getBerries']));
}));

it('checks that user got 5 berries for completing unit', function() {
  var lu = new LearningUnit();
  lu.completeUnit();
  // check that user got 5 berries for completing unit
  expect(berryService.getBerries).toHaveBeenCalledWith(5);
});

// LearningUnit.js
app.service("LearningUnit", function(berryService, User, lrsService, ...) {
  this.completeUnit = function() { berryService.getBerries(5); };
}
  • inject spies / mocks easily - globally
  • controllers easily testable (no DOM)

Maintainability

compiling HTML templates

angular.module("template/accordion/accordion-group.html", [])
.run(["$templateCache", function($templateCache) {
  $templateCache.put("template/accordion/accordion-group.html",
    "
\n" + " \n" + "
\n" + "
\n" + "
"); }]);
  • Busuu - 177 HTML templates - one per file
  • HTML in JS code not maintainable
  • grunt-html2js
  • 1 request for all templates
  • testing directives


https://github.com/karlgoldstein/grunt-html2js

Maintainability

annotate dependency injection

app.factory("LearningUnit", function($http, $q, $rootScope, courseService, statsService,
       berryService, lrsService, backend, util, User, lang, myvocab) {
  // dependencies injected based on arguments names - AngularJS internally
  });

// after minification
app.factory("LearningUnit", function(a, b, c, d...) {
  // dependencies can't be injected based on arguments
});

// after annotation
app.factory("LearningUnit", ['$http', '$q', '$rootScope', 'courseService', 'berryService',
  'lrsService', 'backend', 'util', 'User', 'lang', 'myvocab',
  function($http, $q, $rootScope, courseService, statsService, berryService,
  lrsService, backend, util, User, lang, myvocab) {
  // body
}]);
  • Busuu - > 150 services
  • minification breaks DI
  • need to update DI array
  • argument name != injected service name
  • ngmin - AngularJS pre-minifier


https://github.com/btford/ngmin

Maintainability

linking JS files

  
  
  
  
  
  ...
  
  
  
  

  
$ grunt sails-linker
  • one service / directive / controller per file


https://www.npmjs.org/package/grunt-sails-linker

Dirty checking (vs accessors)

// Backbone
person.set({name: 'Filip'});
// model knows on every set invoke that there was a change to person.name

// AngularJS
$scope.$apply(function() {
  person.name = 'Filip';
});
// AngularJS needs to compare 'person.name' (and all other model variables) to last known value to detect change
  • no accessors (get / set)
  • need to check all watch expressions

Dirty checking (vs accessors)

Nice:

  • models are Plain Old Javascript Objects
  • UI change coalescence (update UI only once, after model stabilise)

Potential drawbacks (performance):

  • too many variables to compare (watchers)
  • comparisons too often (digest cycles)
  • slow comparisons (strings, arrays)

Performance (dirty checking)

potentially slow code

// HTML interpolated, naive approach
{{ translate('Congratulations. You finished a grammar unit.') }}

// HTML directive
Congratulations. You finished a grammar unit.

// JavaScript
app.directive('trs', function(translate) {
  return {
    link: function(scope, elm) {
      var translated = translate(elm.text());
      elm.replaceWith(translated);
    }
  }
}
  • translate() might be slow
  • string comparison is expensive
  • better translate on link phase

Performance (dirty checking)

change language on runtime

// HTML interpolated
{{ translate('Congratulations. You finished a grammar unit.') }}

// HTML directive
Congratulations. You finished a grammar unit.

// JavaScript
app.directive('trs', function(translate) {
  return {
    link: function(scope, elm) {
      var sourceText = elm.text();
      function fill() {
        var translated = translate(sourceText);
        elm.html(translated);
      }

      scope.$on('languageChanged', fill);
      fill();
    }
  }
}
  • Busuu - 12 interface languages

Performance (dirty checking)

too many watchers

app.directive('removeWatchers', function($timeout) {
    return {
      scope: false,
      link: function(scope, elm, attrs) {
        function removeWatchers(scope) {
          // remove watchers from scope.$$watchers and all scope children
          ...
        }

        $timeout(function() {
          // this will execute after first digest cycle
      	  removeWatchers();
        });
      }
    };
  });
    
{{ unit.title }}
  • Busuu dashboard
  • rough solution - removes ALL watchers


Performance (dirty checking)

too many watchers

  • more fine-grained solution
  • 1-1 match with AngularJS directives - ng-if, ng-bind, ng-class etc.
  • ng-bind - once-bind
  • ng-class - once-class
  • ng-if - once-if


https://github.com/tadeuszwojcik/angular-once

Performance (dirty checking)

too many digest cycles

// not a good idea!
$(body).onScroll(function() {
  scope.$apply(function() {
    updateUI();
  });
});

// better, executed at most once every 300msec
var throttled = _.throttle(function() {
  scope.$apply(function() {
    updateUI();
  });
}, 300);
  • use scope.$apply for model modification
  • frequent changes in view (like onScroll) outside AngularJS context

Performance (dirty checking)

Scary? Object.observe spec

var beingWatched = {};
// Define callback function to get notified on changes
function somethingChanged(changes) {
    // do something
}

Object.observe(beingWatched, somethingChanged);
  • Chrome Canary
  • part of ECMA Script 7
  • no more dirty checking

Thank you. Questions?

Slides: http://filipsobczak.com/slides_list/codeshow
Slides created with: Reveal.JS