20 min read

In this article by Chandermani, the author of AngularJS by Example, we focus our discussion on the performance aspect of AngularJS. For most scenarios, we can all agree that AngularJS is insanely fast. For standard size views, we rarely see any performance bottlenecks. But many views start small and then grow over time. And sometimes the requirement dictates we build large pages/views with a sizable amount of HTML and data. In such a case, there are things that we need to keep in mind to provide an optimal user experience.

Take any framework and the performance discussion on the framework always requires one to understand the internal working of the framework. When it comes to Angular, we need to understand how Angular detects model changes. What are watches? What is a digest cycle? What roles do scope objects play? Without a conceptual understanding of these subjects, any performance guidance is merely a checklist that we follow without understanding the why part.

Let’s look at some pointers before we begin our discussion on performance of AngularJS:

  • The live binding between the view elements and model data is set up using watches. When a model changes, one or many watches linked to the model are triggered. Angular’s view binding infrastructure uses these watches to synchronize the view with the updated model value.
  • Model change detection only happens when a digest cycle is triggered.
  • Angular does not track model changes in real time; instead, on every digest cycle, it runs through every watch to compare the previous and new values of the model to detect changes.
  • A digest cycle is triggered when $scope.$apply is invoked. A number of directives and services internally invoke $scope.$apply:
    • Directives such as ng-click, ng-mouse* do it on user action
    • Services such as $http and $resource do it when a response is received from server
    • $timeout or $interval call $scope.$apply when they lapse
  • A digest cycle tracks the old value of the watched expression and compares it with the new value to detect if the model has changed. Simply put, the digest cycle is a workflow used to detect model changes.
  • A digest cycle runs multiple times till the model data is stable and no watch is triggered.

Once you have a clear understanding of the digest cycle, watches, and scopes, we can look at some performance guidelines that can help us manage views as they start to grow.

(For more resources related to this topic, see here.)

Performance guidelines

When building any Angular app, any performance optimization boils down to:

  • Minimizing the number of binding expressions and hence watches
  • Making sure that binding expression evaluation is quick
  • Optimizing the number of digest cycles that take place

The next few sections provide some useful pointers in this direction.

Remember, a lot of these optimization may only be necessary if the view is large.

Keeping the page/view small

The sanest advice is to keep the amount of content available on a page small. The user cannot interact/process too much data on the page, so remember that screen real estate is at a premium and only keep necessary details on a page.

The lesser the content, the lesser the number of binding expressions; hence, fewer watches and less processing are required during the digest cycle. Remember, each watch adds to the overall execution time of the digest cycle. The time required for a single watch can be insignificant but, after combining hundreds and maybe thousands of them, they start to matter.

Angular’s data binding infrastructure is insanely fast and relies on a rudimentary dirty check that compares the old and the new values. Check out the stack overflow (SO) post (http://stackoverflow.com/questions/9682092/databinding-in-angularjs), where Misko Hevery (creator of Angular) talks about how data binding works in Angular.

Data binding also adds to the memory footprint of the application. Each watch has to track the current and previous value of a data-binding expression to compare and verify if data has changed.

Keeping a page/view small may not always be possible, and the view may grow. In such a case, we need to make sure that the number of bindings does not grow exponentially (linear growth is OK) with the page size. The next two tips can help minimize the number of bindings in the page and should be seriously considered for large views.

Optimizing watches for read-once data

In any Angular view, there is always content that, once bound, does not change. Any read-only data on the view can fall into this category. This implies that once the data is bound to the view, we no longer need watches to track model changes, as we don’t expect the model to update.

Is it possible to remove the watch after one-time binding? Angular itself does not have something inbuilt, but a community project bindonce (https://github.com/Pasvaz/bindonce) is there to fill this gap.

Angular 1.3 has added support for bind and forget in the native framework. Using the syntax {{::title}}, we can achieve one-time binding. If you are on Angular 1.3, use it!

Hiding (ng-show) versus conditional rendering (ng-if/ng-switch) content

You have learned two ways to conditionally render content in Angular. The ng-show/ng-hide directive shows/hides the DOM element based on the expression provided and ng-if/ng-switch creates and destroys the DOM based on an expression.

For some scenarios, ng-if can be really beneficial as it can reduce the number of binding expressions/watches for the DOM content not rendered. Consider the following example:

<div ng-if='user.isAdmin'>
   <div ng-include="'admin-panel.html'"></div>
</div>

The snippet renders an admin panel if the user is an admin. With ng-if, if the user is not an admin, the ng-include directive template is neither requested nor rendered saving us of all the bindings and watches that are part of the admin-panel.html view.

From the preceding discussion, it may seem that we should get rid of all ng-show/ng-hide directives and use ng-if. Well, not really! It again depends; for small size pages, ng-show/ng-hide works just fine. Also, remember that there is a cost to creating and destroying the DOM. If the expression to show/hide flips too often, this will mean too many DOMs create-and-destroy cycles, which are detrimental to the overall performance of the app.

Expressions being watched should not be slow

Since watches are evaluated too often, the expression being watched should return results fast.

The first way we can make sure of this is by using properties instead of functions to bind expressions. These expressions are as follows:

{{user.name}}
ng-show='user.Authorized'

The preceding code is always better than this:

{{getUserName()}}
ng-show = 'isUserAuthorized(user)'

Try to minimize function expressions in bindings. If a function expression is required, make sure that the function returns a result quickly. Make sure a function being watched does not:

  • Make any remote calls
  • Use $timeout/$interval
  • Perform sorting/filtering
  • Perform DOM manipulation (this can happen inside directive implementation)
  • Or perform any other time-consuming operation

Be sure to avoid such operations inside a bound function.

To reiterate, Angular will evaluate a watched expression multiple times during every digest cycle just to know if the return value (a model) has changed and the view needs to be synchronized.

Minimizing the deep model watch

When using $scope.$watch to watch for model changes in controllers, be careful while setting the third $watch function parameter to true. The general syntax of watch looks like this:

$watch(watchExpression, listener, [objectEquality]);

In the standard scenario, Angular does an object comparison based on the reference only. But if objectEquality is true, Angular does a deep comparison between the last value and new value of the watched expression. This can have an adverse memory and performance impact if the object is large.

Handling large datasets with ng-repeat

The ng-repeat directive undoubtedly is the most useful directive Angular has. But it can cause the most performance-related headaches. The reason is not because of the directive design, but because it is the only directive that allows us to generate HTML on the fly. There is always the possibility of generating enormous HTML just by binding ng-repeat to a big model list. Some tips that can help us when working with ng-repeat are:

  • Page data and use limitTo: Implement a server-side paging mechanism when a number of items returned are large.

    Also use the limitTo filter to limit the number of items rendered. Its syntax is as follows:

    <tr ng-repeat="user in users |limitTo:pageSize">…</tr>

    Look at modules such as ngInfiniteScroll (http://binarymuse.github.io/ngInfiniteScroll/) that provide an alternate mechanism to render large lists.

  • Use the track by expression: The ng-repeat directive for performance tries to make sure it does not unnecessarily create or delete HTML nodes when items are added, updated, deleted, or moved in the list. To achieve this, it adds a $$hashKey property to every model item allowing it to associate the DOM node with the model item.

    We can override this behavior and provide our own item key using the track by expression such as:

    <tr ng-repeat="user in users track by user.id">…</tr>

    This allows us to use our own mechanism to identify an item. Using your own track by expression has a distinct advantage over the default hash key approach. Consider an example where you make an initial AJAX call to get users:

    $scope.getUsers().then(function(users){ $scope.users = users;})

    Later again, refresh the data from the server and call something similar again:

    $scope.users = users;

    With user.id as a key, Angular is able to determine what elements were added/deleted and moved; it can also determine created/deleted DOM nodes for such elements. Remaining elements are not touched by ng-repeat (internal bindings are still evaluated). This saves a lot of CPU cycles for the browser as fewer DOM elements are created and destroyed.

  • Do not bind ng-repeat to a function expression: Using a function’s return value for ng-repeat can also be problematic, depending upon how the function is implemented.

    Consider a repeat with this:

    <tr ng-repeat="user in getUsers()">…</tr>

    And consider the controller getUsers function with this:

    $scope.getUser = function() {
       var orderBy = $filter('orderBy');
       return orderBy($scope.users, predicate);
    }

    Angular is going to evaluate this expression and hence call this function every time the digest cycle takes place. A lot of CPU cycles were wasted sorting user data again and again. It is better to use scope properties and presort the data before binding.

  • Minimize filters in views, use filter elements in the controller: Filters defined on ng-repeat are also evaluated every time the digest cycle takes place. For large lists, if the same filtering can be implemented in the controller, we can avoid constant filter evaluation. This holds true for any filter function that is used with arrays including filter and orderBy.

Avoiding mouse-movement tracking events

The ng-mousemove, ng-mouseenter, ng-mouseleave, and ng-mouseover directives can just kill performance. If an expression is attached to any of these event directives, Angular triggers a digest cycle every time the corresponding event occurs and for events like mouse move, this can be a lot.

We have already seen this behavior when working with 7 Minute Workout, when we tried to show a pause overlay on the exercise image when the mouse hovers over it.

Avoid them at all cost. If we just want to trigger some style changes on mouse events, CSS is a better tool.

Avoiding calling $scope.$apply

Angular is smart enough to call $scope.$apply at appropriate times without us explicitly calling it. This can be confirmed from the fact that the only place we have seen and used $scope.$apply is within directives.

The ng-click and updateOnBlur directives use $scope.$apply to transition from a DOM event handler execution to an Angular execution context. Even when wrapping the jQuery plugin, we may require to do a similar transition for an event raised by the JQuery plugin.

Other than this, there is no reason to use $scope.$apply. Remember, every invocation of $apply results in the execution of a complete digest cycle.

The $timeout and $interval services take a Boolean argument invokeApply. If set to false, the lapsed $timeout/$interval services does not call $scope.$apply or trigger a digest cycle. Therefore, if you are going to perform background operations that do not require $scope and the view to be updated, set the last argument to false.

Always use Angular wrappers over standard JavaScript objects/functions such as $timeout and $interval to avoid manually calling $scope.$apply. These wrapper functions internally call $scope.$apply.

Also, understand the difference between $scope.$apply and $scope.$digest. $scope.$apply triggers $rootScope.$digest that evaluates all application watches whereas, $scope.$digest only performs dirty checks on the current scope and its children. If we are sure that the model changes are not going to affect anything other than the child scopes, we can use $scope.$digest instead of $scope.$apply.

Lazy-loading, minification, and creating multiple SPAs

I hope you are not assuming that the apps that we have built will continue to use the numerous small script files that we have created to separate modules and module artefacts (controllers, directives, filters, and services). Any modern build system has the capability to concatenate and minify these files and replace the original file reference with a unified and minified version. Therefore, like any JavaScript library, use minified script files for production.

The problem with the Angular bootstrapping process is that it expects all Angular application scripts to be loaded before the application can bootstrap. We cannot load modules, controllers, or in fact, any of the other Angular constructs on demand. This means we need to provide every artefact required by our app, upfront.

For small applications, this is not a problem as the content is concatenated and minified; also, the Angular application code itself is far more compact as compared to the traditional JavaScript of jQuery-based apps. But, as the size of the application starts to grow, it may start to hurt when we need to load everything upfront.

There are at least two possible solutions to this problem; the first one is about breaking our application into multiple SPAs.

Breaking applications into multiple SPAs

This advice may seem counterintuitive as the whole point of SPAs is to get rid of full page loads. By creating multiple SPAs, we break the app into multiple small SPAs, each supporting parts of the overall app functionality.

When we say app, it implies a combination of the main (such as index.html) page with ng-app and all the scripts/libraries and partial views that the app loads over time.

For example, we can break the Personal Trainer application into a Workout Builder app and a Workout Runner app. Both have their own start up page and scripts. Common scripts such as the Angular framework scripts and any third-party libraries can be referenced in both the applications. On similar lines, common controllers, directives, services, and filters too can be referenced in both the apps.

The way we have designed Personal Trainer makes it easy to achieve our objective. The segregation into what belongs where has already been done.

The advantage of breaking an app into multiple SPAs is that only relevant scripts related to the app are loaded. For a small app, this may be an overkill but for large apps, it can improve the app performance.

The challenge with this approach is to identify what parts of an application can be created as independent SPAs; it totally depends upon the usage pattern of the application.

For example, assume an application has an admin module and an end consumer/user module. Creating two SPAs, one for admin and the other for the end customer, is a great way to keep user-specific features and admin-specific features separate. A standard user may never transition to the admin section/area, whereas an admin user can still work on both areas; but transitioning from the admin area to a user-specific area will require a full page refresh.

If breaking the application into multiple SPAs is not possible, the other option is to perform the lazy loading of a module.

Lazy-loading modules

Lazy-loading modules or loading module on demand is a viable option for large Angular apps. But unfortunately, Angular itself does not have any in-built support for lazy-loading modules.

Furthermore, the additional complexity of lazy loading may be unwarranted as Angular produces far less code as compared to other JavaScript framework implementations. Also once we gzip and minify the code, the amount of code that is transferred over the wire is minimal.

If we still want to try our hands on lazy loading, there are two libraries that can help:

With lazy loading in place, we can delay the loading of a controller, directive, filter, or service script, until the page that requires them is loaded.

The overall concept of lazy loading seems to be great but I’m still not sold on this idea. Before we adopt a lazy-load solution, there are things that we need to evaluate:

  • Loading multiple script files lazily: When scripts are concatenated and minified, we load the complete app at once. Contrast it to lazy loading where we do not concatenate but load them on demand. What we gain in terms of lazy-load module flexibility we lose in terms of performance. We now have to make a number of network requests to load individual files.

    Given these facts, the ideal approach is to combine lazy loading with concatenation and minification. In this approach, we identify those feature modules that can be concatenated and minified together and served on demand using lazy loading. For example, Personal Trainer scripts can be divided into three categories:

    • The common app modules: This consists of any script that has common code used across the app and can be combined together and loaded upfront
    • The Workout Runner module(s): Scripts that support workout execution can be concatenated and minified together but are loaded only when the Workout Runner pages are loaded.
    • The Workout Builder module(s): On similar lines to the preceding categories, scripts that support workout building can be combined together and served only when the Workout Builder pages are loaded.

    As we can see, there is a decent amount of effort required to refactor the app in a manner that makes module segregation, concatenation, and lazy loading possible.

  • The effect on unit and integration testing: We also need to evaluate the effect of lazy-loading modules in unit and integration testing. The way we test is also affected with lazy loading in place. This implies that, if lazy loading is added as an afterthought, the test setup may require tweaking to make sure existing tests still run.

Given these facts, we should evaluate our options and check whether we really need lazy loading or we can manage by breaking a monolithic SPA into multiple smaller SPAs.

Caching remote data wherever appropriate

Caching data is the one of the oldest tricks to improve any webpage/application performance. Analyze your GET requests and determine what data can be cached. Once such data is identified, it can be cached from a number of locations.

Data cached outside the app can be cached in:

  • Servers: The server can cache repeated GET requests to resources that do not change very often. This whole process is transparent to the client and the implementation depends on the server stack used.
  • Browsers: In this case, the browser caches the response. Browser caching depends upon the server sending HTTP cache headers such as ETag and cache-control to guide the browser about how long a particular resource can be cached. Browsers can honor these cache headers and cache data appropriately for future use.

If server and browser caching is not available or if we also want to incorporate any amount of caching in the client app, we do have some choices:

  • Cache data in memory: A simple Angular service can cache the HTTP response in the memory. Since Angular is SPA, the data is not lost unless the page refreshes. This is how a service function looks when it caches data:
    var workouts;
    service.getWorkouts = function () {
       if (workouts) return $q.resolve(workouts);
       return $http.get("/workouts").then(function (response)
    {
           workouts = response.data;
           return workouts;
       });
    };

    The implementation caches a list of workouts into the workouts variable for future use. The first request makes a HTTP call to retrieve data, but subsequent requests just return the cached data as promised. The usage of $q.resolve makes sure that the function always returns a promise.

  • Angular $http cache: Angular’s $http service comes with a configuration option cache. When set to true, $http caches the response of the particular GET request into a local cache (again an in-memory cache). Here is how we cache a GET request:
    $http.get(url, { cache: true});

    Angular caches this cache for the lifetime of the app, and clearing it is not easy. We need to get hold of the cache dedicated to caching HTTP responses and clear the cache key manually.

The caching strategy of an application is never complete without a cache invalidation strategy. With cache, there is always a possibility that caches are out of sync with respect to the actual data store.

We cannot affect the server-side caching behavior from the client; consequently, let’s focus on how to perform cache invalidation (clearing) for the two client-side caching mechanisms described earlier.

If we use the first approach to cache data, we are responsible for clearing cache ourselves.

In the case of the second approach, the default $http service does not support clearing cache. We either need to get hold of the underlying $http cache store and clear the cache key manually (as shown here) or implement our own cache that manages cache data and invalidates cache based on some criteria:

var cache = $cacheFactory.get('$http');
cache.remove("http://myserver/workouts"); //full url

Using Batarang to measure performance

Batarang (a Chrome extension), as we have already seen, is an extremely handy tool for Angular applications. Using Batarang to visualize app usage is like looking at an X-Ray of the app. It allows us to:

  • View the scope data, scope hierarchy, and how the scopes are linked to HTML elements
  • Evaluate the performance of the application
  • Check the application dependency graph, helping us understand how components are linked to each other, and with other framework components.

If we enable Batarang and then play around with our application, Batarang captures performance metrics for all watched expressions in the app. This data is nicely presented as a graph available on the Performance tab inside Batarang:AngularJS by Example

That is pretty sweet!

When building an app, use Batarang to gauge the most expensive watches and take corrective measures, if required.

Play around with Batarang and see what other features it has. This is a very handy tool for Angular applications.

This brings us to the end of the performance guidelines that we wanted to share in this article. Some of these guidelines are preventive measures that we should take to make sure we get optimal app performance whereas others are there to help when the performance is not up to the mark.

Summary

In this article, we looked at the ever-so-important topic of performance, where you learned ways to optimize an Angular app performance.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here