Optimizing Angular Apps

Angular is clearly great for optimizing development time, but how does it stack up in terms of performance, and what can we do to get it to be as fast as possible?

For most web applications, the native speed of Angular is fast enough, and don’t need to pay any special attention to optimizing its performance. When our applications get slow or show poor performance, we can then attack optimizing our Angular apps.

What to Optimize

In order to know where to focus on the optimizing our app, we need to understand what’s happening under the Angular hood. As with any applications, we start by focusing on the cause of our issues.

Optimizing the $digest Loop

The obvious place to start looking for performance issues is in the $digest loop. In short, Angular runs through a watch list that is responsible for keeping track of the live data bindings. Every single piece of live data that can possibly change on the page has a watch applied to it.

The Under the Hood chapter discusses the $watch list and the $digest loop in detail.

Each of these watches causes the $digest loop to take more time to finish rendering, as Angular needs to keep track of the value and check if it’s changed on each loop.

Focusing on limiting the number of unnecessary watches will gain us the biggest performance boosts. Additionally, keeping the bi-directional data comparisons simple will give us even more performance boosts, because the browser can check these quickly.

In our apps, we should be mindful that the number of bi-directional data bindings should not exceed more than 2000 data bindings on the page for each $digest loop.

Sometimes we don’t even want the full $digest loop to run on our app. For instance, imagine we have a polling loop that checks our server several times a second. If we receive a websocket event that fires a full digest loop with every message, we’ll have a pretty slow application.

We recommend using websockets, as they are more production-friendly and less prone to errors.

app.factory('poller', function($rootScope, $http) {
  var pollForEvent = function(timeout) {
    $http.get('/events')
    .success(function(data) {
      var events = data.events;
      for (var i = 0; i < events.length; i++) {
        var event = events[i];
        if (service.handlers[event])
          for (handler in service.handlers[event])
            $rootScope.$apply(function() {
              handler.apply(event);
            });
      }
      // Set the next timeout
      setTimeout(pollForEvent, timeout);
    });
  };
  // poll every half-second
  setTimeout(function() { pollForEvents(500); });
  var service = {
    handlers: {},
    on: function(evt, callback) {
      if (!service.handlers[evt]) 
        service.handlers[evt] = [];
        service.handlers[evt].push(callback);
    }
  }
  return service;
});

The major issue with this code is that we’ll end up running the $rootScope.$apply() method for every single event that gets fired, which can add up to many $digest loops per second.

Limiting the number of $digest loops per second is a great way to start upgrading the performance of our app. We can throttle the events to happen only at a maximum of times we want per second.

// throttle function
var throttle = function(fn, atMost, ctx) {
  var ctx     = ctx || this;
  var atMost  = atMost || 250; // milliseconds
  var last, defer, result;
  return function() {
    var now = new Date(),
        args = arguments;
    if (last && now < last + atMost) {
      // Execute this later
      clearTimeout(defer);
      defer = setTimeout(function() {
        last = now;
        fn.apply(ctx, args);
      }, atMost);
    } else {
      result = fn.apply(ctx, args);
    }
    return result;
  }
}

This relatively ugly throttle() function only triggers the function once, at most, per atMost cycle.

The Underscore.js library has a much better production-ready, battle-tested version of this code.

To set our $digest loop to throttle using our throttle function, we can simply invoke it in the event loop:

// ...
for (var i = 0; i < events.length; i++) {
  var event = events[i];
  if (service.handlers[event])
    for (handler in service.handlers[event])
      throttle(function() {
        $rootScope.$apply(function() {
          handler.apply(event);
        });
      }, 500);
}

Optimizing ng-repeat

One of the biggest sources of delay in Angular is the ng-repeat directive. For every single element that ng-repeat places, there will be at least one data binding per entry in the list, and this fact does not even count any of the bindings that we create inside of the list elements.

Let’s take a look at the performance of the following repeating list generated by ng-repeat:

<ul>
  <li ng-repeat="email in emails">
    <a ng-href="#/from/{{ email.sender }}">
      {{ email.sender }}
    </a>
    <a ng-href="#/email/{{ email.id }}">
      {{ email.subject }}
    </a>
  </li>
</ul>

For every single email in our list, we’re going to have, at minimum, one watch generated by the ngRepeat directive (this watch monitors the list for changes). Since Angular creates a $watch for every single ng directive, the above list has 4 + 1 watches per email. For a list of 100 emails, the usages above already creates 500 watches, and the above example list is not even a complex one for the entire page.

With this relatively short list, it’s obvious to see how the performance of the app can be greatly reduced with any significantly sized app. There are some relatively simple ways that we can speed up our application.

Optimizing the $digest Call

We can often determine when and to which scope(s) running the $digest loop will affect when we change a variable. When this is the case, we don’t need to invoke the entire $digest loop on the $rootScope using $scope.$apply() (which causes every child $scope to run in the $digest loop). Instead, we can directly call $scope.$digest().

Calling $scope.$digest() only runs the digest loop on the specific scope that called $digest() and all of its children.

Optimizing $watch Functions

Since the $watch list expressions are executed every $digest loop, it’s important that we keep the functionality tiny. The smaller and more focused the $watch expression, the more performant our application will be.

Avoiding deep comparisons, complex logic, and any loops in our $watch() functions will help speed up our applications.

For instance, we could set up a watch that watches an object. Imagine we have an Account object:

$scope.account = {
  active: true,
  userId: 123,
  balance: 1000 // in cents
}

Presumably, we’d want to watch for any time the balance changes and set the account to not active if the balance reaches zero. We can set up a $watch function that watches the account object and updates the account whenever the balance object changes:

$scope.$watch('user', function(newAccount) {
  if (newAccount.balance <= 0) {
    $scope.account.active = false
  }
}, true);

The third argument in the $watch() function tells Angular to watch the object using deep comparison, checking every property with the angular.equals() function.

This choice will cause terrible performance. Not only is Angular making a copy of the object, but it stores and saves it while it needs to walk through every property to check if any of them have changed it.

A tip for building our $watch functions: Use them to keep track of variables that clearly affect the view. Anything that does not affect the view does not need a $watch function.

Sometimes it makes sense for us to remove watchers, specifically when a $watch function becomes irrelevant because the data is static and we just want to expose it in our view the first time.

We can easily remove custom watchers from our view: The $watch function itself returns a function that enables us to remove the $watch function.

For instance, say we have a custom directive that is waiting for the resolution of a variable name:

<div my-directive name="customerName"></div>
 
This page is a preview of ng-book.
Get the rest of this chapter plus 600 pages of the best Angular content on the web.

 

Ready to master AngularJS?

  • What if you could master the entire framework – with solid foundations – in less time without beating your head against a wall? Imagine how quickly you could work if you knew the best practices and the best tools?
  • Stop wasting your time searching and have everything you need to be productive in one, well-organized place, with complete examples to get your project up without needing to resort to endless hours of research.
  • You will learn what you need to know to work professionally with ng-book: The Complete Book on AngularJS or get your money back.
Get it now