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>
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