Optimizing Angular App Bindings

Home / Optimizing Angular App Bindings

One of the best features of Angular is also one of its most impactful on performance. That feature is two-way binding.

To achieve two-way binding Angular “watches” for when JavaScript objects change and when the DOM values change during Angular’s digest cycle. It’s not an extremely complex mechanism, conceptually, but it’s very effective.


Angular’s $watch provider/service, as a result, can really slow down your application. So, when I see an Angular application getting sluggish, I immediately start examining how many watchers are running during each digest cycle. Imagine if you’re displaying a table on your page with 10 columns and 1000 rows. You’d wind up with 10,000+ watchers.

The big question, then, is what can you do about? Before answering that, I find it’s good to have mechanisms in place to know how many watchers are present.

To that end, I wrote a simple service that I put into most of my web applications that can simply be toggled on or off based on whether I’m running in a development or production environment.

Whenever Angular binds an element, the DOM element gets an “ng-scope” class attached to it. For each of those elements, we can look at their $scope and the scope’s $$watchers – which provides a count of watchers on that particular scope. This gives us a pretty convenient path for determining the number of watchers on a view. Sure, it’s not exact, but it’s close enough to get a rough estimate.

Let’s jump right to the service code.

(function () {
    var watchCount = function() {
        // I return the count of watchers on the current page.
        function getWatchCount() {
            var total = 0;
            angular.element( ".ng-scope" ).each(
                function countWatchers() {
                    var scope = $(this).scope();
                    total += scope.$$watchers
                        ? scope.$$watchers.length
                        : 0;
                }
            );

            return( total );
        }

        return {
            getWatchCount: getWatchCount
        };
    }

    angular.module('myApp.services')
        .factory('watchCountService', watchCount);
})()

As you can see, all we’re doing is using, effectively, a jQuery selector to find elements with the “ng-scope” class. Then, we iterate through each of those elements and sum up the $$watchers on each element’s $scope. Any controller/service that has our watchCount service injected into it can get the number of watchers on the view. What I do with this is generally have my “menuController” (if you’ll harkin back to my previous post on the menuService / menuController to provide state-based navigation) to display this count within the menu.

Within our controller, we need to watch for the watch count change since it will change as we change states/views.

$scope.$watch(
    function watchCountExpression() {
        return (watchCountService.getWatchCount());
    },
    function handleWatchCountChange(newValue) {
        vm.watchCount = newValue;
    });

All that is left from here is to bind our “vm.watchCount” to our view. As I mentioned, I toggle the display of this based on my environment. Below is a jsFiddle illustrating the usage and a simple test case containing (4) states. The first state doesn’t really display much and has few watches. States 2-4, though, display a table bound with data through an ng-repeat. Progressing through the states increases the row count. These states include a text box in their views to let you type. You’ll notice that as the row count increases that even the response of typing into the text box becomes very sluggish.

Now the big question is, what can we do to increase performance in the scenarios where many watch counts seem unavoidable. I begin dissecting my data and determining if I really need watchers. There are also directives, such as one that I really like, called Bindonce that can reduce watch counts significantly. The source code is available so it’s easy enough to see how the author achieved a reduction in watch counts and is a good place to start. Some caveats do apply with this directive, though. You can, for example, have undesired side-effects from reducing/eliminating watchers.

In some circumstances, where I can rely more on DOM or Angular events to indicate changes, I’ll replace watches/bindings in my directives with event-driven mechanisms and then map data to the appropriate objects as possible. One example of this is a “tri-state” checkbox that lets users select/deselect all rows within a table. Having watchers on every row to track this state change is very costly. But using DOM events reduced the watchers to only 2-3 total. Performance increased exponentially.

Update

Here some other suggestions (thanks to Helgevold for summary):

One way binding
Just like the Bindonce plugin that I mentioned previously, we can take advantage of Angular’s (v1.3+) built-in mechanism for one-way binding. The syntax is to use {{ :: }}. So, for example if we wanted to bind once to the firstName of a user, it would look like this:

{{ ::user.firstName }}

The nice thing about this method is that it can be used within ng-repeats to further reduce watch counts.

ng-if or ng-show
Interestingly, there is a big difference between ng-if and ng-show. ng-show simply hides/shows rendered DOM elements. This means that even if we aren’t showing elements through ng-show, they’re still rendered and, thus, will still have watches associated with them. ng-if completely removes hidden content from the DOM. This effectively means that content that has been hidden /removed from the DOM by ng-if will not have watches associated with it. For complex conditionally rendered blocks, this could be a fairly significant reduction in watches.

These are some of the more effective ways to reduce watches significantly. But, please read through the included link to Helgevold for more suggestions.

I’m happy to discuss other performance enhancements and would really like to hear your ideas for and experiences in increasing binding/watcher performance.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.