Improve the performance of your AngularJS application

-

AngularJS is a huge framework and already has many performance enhancements built in. Despite that, it’s very easy to write code in AngularJS that will slow down your application. To avoid this, it is important to understand what causes an AngularJS application to slow down and to be aware of trade-offs that are made in the development process.

In this post I describe things that I have learned from developing AngularJS applications that will hopefully enable you to build faster applications.

First of all, a quick win to boost your applications performance. By default AngularJS attaches information about binding and scopes to DOM nodes and adds CSS classes to data-bound elements. The application doesn’t need this information but it is added for debugging tools like Batarang to work properly. You can disable this by one simple rule in your application config.

app.config(['$compileProvider', function ($compileProvider) { 
    $compileProvider.debugInfoEnabled(false); 
}]);

If you want to temporarily enable the debug information just open the debug console and call this method directly in the console:

angular.reloadWithDebugInfo();

Watchers in AngularJS

“With great power comes great responsibility”

AngularJS provides the $watch API to observe changes in the application. To keep track of changes you need to set a watcher. Watchers are created on:

  • $scope.$watch.
  • {{  }} type bindings.
  • Directives like ngShow, ngClass, ngBindHtml, etc.
  • Isolated scope variables: scope: { foo: ’=’ }.
  • filters.

Basically: everything in AngularJS uses watchers.

AngularJS uses dirty checking, this means it goes through every watcher to check if they need to be updated. This is called the digest loop. If a watcher relies on another watcher, the digest loop is called again, to make sure that all of the changes have propagated. It will continue to do so, until all of the watchers have been updated.

The digest loop runs on:

  • User actions (ngClick, ngModel, ngChange).
  • $http responses.
  • promises are resolved.
  • using $timeout/$interval.
  • calling $scope.$apply() of $scope.$digest().

Basically: the digest loop is called a lot.

The “Magic” of AngularJS works great but it is fairly easy to add so many watchers, that your app will slow down. Especially when watchers doing too much work. With this in mind we will go on and talk about some easy changes to make our applications faster!

Avoid a deep watch

By default, the $watch() function only checks object reference equality. This means that within each $digest, AngularJS will check to see if the new and old values pointing to the same object. A normal watch is really fast and preferred from a performance point of view.

What if you want to perform some action when a modification happened to an object or array? You can switch to a deep watch. This means that within each $digest, AngularJS will check the whole tree to see if the structure or values are changed. This could be very expensive.

A collection watch could be a better solution. It works like a deep watch, except it only checks the first level of object’s properties and checks for modifications to an array like adding, removing, replacing, and reordering items.  Collection watches are used internally by Angular in the ngRepeat directive

Another effective alternative that I recommend is using one-way dataflow. This can be accomplished by making a copy of the watched object, modify the copy and then change the variable to point to the copy. Than a normal (shallow) watch will do the job.

Use track by in ng-repeat

ngRepeat uses $watchCollection to detect changes in collections. When a change happens, ngRepeat makes the corresponding changes to the DOM.

Let’s say you want to refresh a flight list with new flight data. The obvious implementation for this refresh would be something like this:

 

$scope.flights = serverData.flights;

This would cause ngRepeat to remove all li elements of existing tasks and create them again, which might be expensive if we have a lot of flights or a complex li template. This happens because AngularJS adds a $$hashkey property to every flight object. When you replace the flights with the exact same flights from the server the $$hashkey is missing and ngRepeat won’t know they represent the same elements.

The solution

Use ngRepeat’s track by clause. It allows you to specify your own key for ngRepeat to identify objects, instead of just generating a unique hashkey.

The solution wil look like:

 

Now ngRepeat reuse DOM elements for objects with the same id.

 

Bind once

Use bind once where possible. If bind once is used, Angular will wait for a value to stabilize after it’s first series of digest cycles, and will use that value to render the DOM element. After that, Angular will remove the watcher and forget about that binding. This will minimize the watchers and thereby lightens the $digest loop.

From AngularJS 1.3 there is the :: notation to allow one time binding.

 

 

{{::name}}

       

Use ng-if instead of ng-show/ng-hide

Maybe this is obvious but i think it’s worth mentioning it. Use ng-if where possible. The ng-show/ng-hide directives just hide the element by setting it’s style property “display” to none. The element still exists in the DOM including every watcher inside it. Ng-if removes the element and watchers completely and generates them again when needed.