Video: Optimizing Angular.JS Application

Posted by Ronald Eddy on Wednesday, January 15, 2014

Karl Seamon a Senior Software Engineer at Google looks at performance problems encountered in large angular application and provides:

  1. Basics and Best Practices
  2. Diagnosing performance problems
  3. Improving performance within AngularJS

This video is from ng-conf 2014, Salt Lake City, Utah

Outline

The Problems: What can make an Angular app slow?

  • Anything that could affect a normal JS app
    • Slow DOM
    • Single-threaded
    • etc
  • Inefficient directives *link vs compile *$parse vs $eval vs interpolation *Dirty checking *Lots of watchers *Slow watchers *Too many calls to $apply

What does slow mean?

  • $apply > 25ms
  • Click handler > 100ms
  • Show a new page > 1s
    • > 10s -> users will give up
    • 200ms or less is ideal
  • When a directive appears inside of a repeater
    • compile is called only once
    • link and the constructor are called once per iteration
  • When creating directives, try to get as much work done as possible in the compile step (or even earlier, in the factory)

Directives: Transclusion

  • For directives that wrap other content
  • Allows your directive to $digest its own scope without causing dirty checks on bindings in the user-provided contents

$digest and $apply

  • Angular’s documentation favors $apply
    • Simpler to use $apply all the time
    • $apply has special error handling that $digest lacks
  • So what’s $digest for?
    • $apply = $rootScope.$digest + some other stuff
    • If you update a child scope s, you can call s.$digest to dirty-check only that s and its descendants

Watchers

 
scope.$watch(valueExpression, changeExpression, ...)

valueExpression will be executed many times - Make sure it is fast! Avoid touching the dom (_.debounce can help).

  • $watch has two comparison modes
    • referential (default) - quick, shallow comparison
    • deep - slow, recurses through objects for a deep comparison; also makes a deep copy of the watched value each time it changes
  • Avoid deep $watch whenever possible

$watchCollection

  • new in 1.2, used by ng-repeat
  • Goes one level deep into the watched array or object
  • A nice alternative to deep $watch in many cases

$eval, $parse, and $interpolate

  • $interpolate: returns function that evals “a string { {like} } this”
  • $parse: returns function that evals “an.expression”
  • $scope.$eval: evaluates “an.expression” (using $parse)
  • Better to call $parse once and save the function than to call $eval many times
  • $parse is much faster than $interpolate, prefer it when possible
//unoptimized

for (var i = 0; i < 99999; i++) {
  $scope.$eval(exp);
}

//better
var parsedExp = $parse(exp);
for (var i = 0; i < 99999; i++) {
  parsedExp($scope);
}

Putting it together

//unoptimized

myApp.directive(function() {
  return {
    link: function(s, elem, attr) {
      s.foo = scope.$eval(attr.foo);
      scope.$watch(attr.list,
          function(list) {
            // do something
          }, true);
    }};});

//better
myApp.directive(function($parse) {
  return {
    compile: function(elem, attr) {
      var fooExp = $parse(attr.foo),
          listExp = $parse(attr.list);
      return function link(s, elem) {
        s.foo = fooExp(s);
        scope.$watchCollection(listExp,
            function(list) {
              // do something
            });
      };}};});

$watch only what is needed

Sometimes a deep $watch is needed, but not for the entire object. By stripping out irrelevant data, we can make the comparison much faster.

//unoptimized

$scope.$watch(listOfBigObjects,
    myHandler, true);

//better
$scope.$watch(function($scope) {
  return $scope.listOfBigObjects.
      map(function(bigObject) {
        return bigObject.foo.
            fieldICareAbout;
      });
}, myHandler, true);

$watch before transforming, not after

When applying expensive transformations to input, watch the input itself for changes rather than the output of the transformation.

ng-repeat - track by $index

By default, ng-repeat creates a dom node for each item and destroys that dom node when the item is removed.

With track by $index, it will reuse dom nodes.

//unoptimized

<div ng-repeat=”item in array>
  I live and die by { {item} }.
<div>

//better
<div ng-repeat=”item in array track by $index>
  I live until { {array.length} } is
  less than { {$index} }.
<div>

ng-if vs ng-show

  • ng-show hides elements and bindings using css
  • ng-if goes a step further and does not even create them
    • Fewer bindings
    • Fewer linkers called at startup
  • ng-switch is like ng-if in this respect

Not really a Best Practice: $$postDigest

  • $$ means private to Angular, so be aware that the interface is not stable
  • Fires a callback after the current $digest cycle completes
  • Great for updating the dom once after dirty checking is over

Avoiding dirty checking altogether

  • Sometimes an expression’s output never changes, but we dirty check it constantly
  • Custom directives to the rescue!
    • Bind once at link and ignore digest cycles
    • Bind at link and dirty check only on certain events
    • https://github.com/kseamon/fast-bind/tree/master/
    • Implements bind
    • class, href, if, switch, etc left as an exercise to the reader
      • Or maybe an optional module in 1.3?

Diagnosing performance problems: Tools

  • AngularJS Batarang
    • Great for identifying which $watchers are causing problems
  • Chrome profiler
    • Offers a broader view, but harder to read
  • performance.now()
    • Provides microsecond resolution for measurements

comments powered by Disqus