Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: John Ramon
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: John Ramon@johnramon )

Counting The Number Of Watchers In AngularJS

By Ben Nadel on

AngularJS is magical; but, that magic can come with a performance cost as the size of your AngularJS application grows. The more $watch() bindings you have, the more work AngularJS has to do in every digest. The AngularJS team has stated that this should be fine as long as you keep your $watch() count under 2,000. But, do you really have a good sense of when you reach that tipping point? Bindings can add up much faster than you think! To help build a better mental model, I've created a Bookmarklet that can give you some insight into how many active $watch() bindings are in your current AngularJS page.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

If you look at the AngularJS source code, you will see that each Scope instance stores its watch bindings in a private/proprietary property called, "$$watchers". And, if you look at the rendered source code of your AngularJS web application, you'll see that the DOM (Document Object Model) is decorated with the class name, "ng-scope". These "ng-scope" class names denote which DOM nodes are attached to a scope instance in the $rootScope hierarchy.

Together, we can use these two facts to quickly gather the active scopes in the current page state and then extract the $watch() count from each scope:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Counting The Number Of Watchers In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • .even {
  • background-color: #FAFAFA ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Counting The Number Of Watchers In AngularJS
  • </h1>
  •  
  • <form ng-submit="submitFriend()">
  •  
  • <input type="text" ng-model="form.name" size="30" />
  • <input type="submit" value="Add Friend" />
  •  
  • </form>
  •  
  • <ul>
  • <!--
  • Notice that in my ngRepeat template, I have a number of AngularJS bindings
  • for attribute interpolation, class definition, element visibility, and text
  • output. Obviously, not all of these make sense in this context; but, they are
  • here to show you how fast watch-bindings can add up!!!
  • -->
  • <li
  • ng-repeat="friend in friends track by friend.id"
  • title="Added on {{ friend.createdAt.toDateString() }}"
  • ng-class="{ even: $even, odd: $odd }">
  •  
  • {{ friend.name }} ( {{ friend.id }} )
  •  
  • <em ng-show="$even">
  • ( even row )
  • </em>
  • <em ng-show="$odd">
  • ( odd row )
  • </em>
  •  
  • </li>
  • </ul>
  •  
  • <p>
  • <strong>Watch Count:</strong> {{ watchCount }}
  • </p>
  •  
  • <!--
  • To make this exploration a bit more impactful, I've created a Bookmarklet so
  • you can get the number $watch() bindings on any AngularJS application.
  • -->
  • <p>
  • <em>
  • Get as Bookmarklet:
  • <a bn-bookmarklet="bookmarklet">$watch count</a>
  • &mdash;
  • Anything over 2,000 is considered bad for performance.
  • </em>
  • </p>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, getWatchCount ) {
  •  
  • // I hold the form data for ngModel.
  • $scope.form = {
  • name: ""
  • };
  •  
  • // I hold the current collection of friends (rendered in ngRepeat).
  • $scope.friends = [];
  •  
  • // I hold the current count of watchers in the current page. This extends
  • // beyond the current scope, and will hold the count for all scopes on
  • // the entire page.
  • $scope.watchCount = 0;
  •  
  • // I hold the bookmarkletized version of the function to provide a take-
  • // away feature that can be used on any AngularJS page.
  • $scope.bookmarklet = getWatchCount.bookmarklet;
  •  
  •  
  • // Every time the digest runs, it's possible that we'll change the number
  • // of $watch() bindings on the current page.
  • $scope.$watch(
  • function watchCountExpression() {
  •  
  • return( getWatchCount() );
  •  
  • },
  • function handleWatchCountChange( newValue ) {
  •  
  • $scope.watchCount = newValue;
  •  
  • }
  • );
  •  
  • // Pre-populate some friends.
  • addFriend( "Tricia" );
  • addFriend( "Joanna" );
  • addFriend( "Kit" );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I create a new friend based on the form data and add it to the
  • // current collection (which will alter the state of the ngRepeat).
  • $scope.submitFriend = function() {
  •  
  • // Ignore empty form submissions.
  • if ( ! $scope.form.name ) {
  •  
  • return;
  •  
  • }
  •  
  • addFriend( $scope.form.name );
  •  
  • $scope.form.name = "";
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I add the given friend to the collection.
  • function addFriend( name ) {
  •  
  • $scope.friends.push({
  • id: ( $scope.friends.length + 1 ),
  • name: name,
  • createdAt: new Date()
  • });
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I get a rough estimate of the number of watchers on the page. This assumes
  • // that the entire page is inside the same AngularJS application.
  • app.factory(
  • "getWatchCount",
  • function() {
  •  
  • // I return the count of watchers on the current page.
  • function getWatchCount() {
  •  
  • var total = 0;
  •  
  • // AngularJS denotes new scopes in the HTML markup by appending the
  • // class "ng-scope" to appropriate elements. As such, rather than
  • // attempting to navigate the hierarchical Scope tree, we can simply
  • // query the DOM for the individual scopes. Then, we can pluck the
  • // watcher-count from each scope.
  • // --
  • // NOTE: Ordinarily, it would be a HUGE SIN for an AngularJS service
  • // to access the DOM (Document Object Model). But, in this case,
  • // we're not really building a true AngularJS service, so we can
  • // break the rules a bit.
  • angular.element( ".ng-scope" ).each(
  • function ngScopeIterator() {
  •  
  • // Get the scope associated with this element node.
  • var scope = $( this ).scope();
  •  
  • // The $$watchers value starts out as NULL.
  • total += scope.$$watchers
  • ? scope.$$watchers.length
  • : 0
  • ;
  •  
  • }
  • );
  •  
  • return( total );
  •  
  • }
  •  
  • // For convenience, let's serialize the above method and convert it to
  • // a bookmarklet that can easily be run on ANY AngularJS page.
  • getWatchCount.bookmarklet = (
  • "javascript:alert('Watchers:'+(" +
  • getWatchCount.toString()
  • .replace( /\/\/.*/g, " " )
  • .replace( /\s+/g, " " ) +
  • ")());void(0);"
  • );
  •  
  • return( getWatchCount );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I turn the given scope value into an HREF attribute. This gets around the
  • // automatic sanitization that AngularJS is doing to prevent malicious scripts
  • // from being executed.
  • app.directive(
  • "bnBookmarklet",
  • function() {
  •  
  • // I bind the UI events to the current scope.
  • function link( $scope, element, attributes ) {
  •  
  • element.attr( "href", $scope.$eval( attributes.bnBookmarklet ) );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Normally, you would never want to access the DOM from within an AngularJS service. But, in this case, we can make an exception since we're not really building a true AngularJS service - we're just wiring it up as a service so it can be injected into the demo Controller.

I don't have an exhaustive understanding of the AngularJS source code, so I won't tell you that this approach gives you an exact number. But, I think it will give you, at the very least, a rough estimate of how many $watch() bindings you have and how that number can change with size of your data-set. ngRepeat is probably the biggest offender as it's easy to forget that your ngRepeat template will be multiplied by "N". Just remember, keep those totals under 2,000! And, if you can't, try using a deferred-transclusion approach.




Reader Comments

@Sekib,

I am not too familiar with the $id value. I assume that is some unique identifier associated with each scope. But, I will say that the parent/child relationship of scopes _may_ be irrelevant here. Since we're using the DOM (Document Object Model) as our "source of truth", we can side-step the internal relationship.

If you look at Mike's approach, he's iterating over _all_ DOM elements, which may be why he's checking the $id. My approach queries the DOM for the "ng-scope" class, which should (if I'm reading the documentation correctly) indicate where the unique scopes are bound to the DOM.

But again, I am not sure if this is an exact number - but it should point in the right direction.

Thanks!!

Reply to this Comment

@Sekib,

I think there may be one oversight in my logic. After looking at some DOM elements, I think that the ng-scope class may be added any time that a subtree is transcluded, even if the linked Scope is _not_ unique. I'll have to dig a bit more.

Reply to this Comment

@Ben,

I'll try some other approaches with your example, just to see what I get. There is one more thing I oversaw: $isolateScope. I have to check if this has an impact on $watch count especially when dealing with directives with isolated scope.

Keep writing - I always learn something new on this blog :-)

Reply to this Comment

I remember reading that object.observe() is supposed to result in a tenfold performance increase when processing watches. So that 2,000 watch performance limit may be 20,000 in the future.

I'm interested whether the latest and greatest Angular already uses object.observe() whenever possible. In any case, browser support doesn't seem to be that great: http://kangax.github.io/compat-table/es7/#Object.observe

Reply to this Comment

@László,

I'm very curious to see how Object.observe() will / can actually be integrated. I don't know a lot about the feature (of JavaScript). But, in AngularJS, things are so dynamic and so fluid, I'm not sure how it could be integrated. The whole beauty of the dirty-check is that it brute forces it, which allows all of the objects to be completely changed out in each digest with no ill-effect (since it's all based on names, not references). As such, it is not obvious to me where Object.observe() can be attached in the life-cycle of the view-model.

But, the AngularJS guys are pretty brilliant, so I am excited to see what they do!

Reply to this Comment

@Pete,

I played with Batarang a while back (maybe 2 years ago?). I vaguely remember it crashing my browser or something. I never really got into it. I'll have to check it out though, see what's new!

Reply to this Comment

I included a code snippet based on ng-snippets to count number of watchers for both normal and isolate scopes in my collection of code snippets https://github.com/bahmutov/code-snippets Use them in Chrome DevTools code snippets like this: http://bahmutov.calepin.co/chrome-devtools-code-snippets.html

Reply to this Comment

Hey Ben,

In the meantime I've done some digging and it turned out that Object.observe() won't be integrated into Angular.js 1.x but it will be into 2.x
* https://github.com/angular/angular.js/issues/3601

Looks like it may result in as much as a 40x performance increase on supported browsers!
* https://mail.mozilla.org/pipermail/es-discuss/2012-September/024978.html

Sadly, Object.observe() suport is rather poor on at the moment but once it becomes better it could be a game changer performance-wise.
* https://www.polymer-project.org/resources/compatibility.html

I think it's important to be mindful about the performance issues regarding watch counts. Currently, it's not suggested to have more than or close to 2,000 watches on capable machines in order for the UI to be responsive. According to the above Object.observe() can bump this number to as much as 80,000.

Thanks for your posts, I've learned a ton from you - keep it up!

Reply to this Comment

Theres an project that provides a work around if you are approaching the 2000 thresh hold that attempts to bind better.

https://github.com/Pasvaz/bindonce

The idea being that on any given page, you are using alot of watchers on things that will not change after they are initially populated.

Reply to this Comment

@Gleb,

I just read your article on various profiling and optimization techniques:

http://bahmutov.calepin.co/improving-angular-web-app-performance-example.html

... really high quality stuff! Also, I didn't know that Chrome DevTools had snippets :) Something new to dig into!

Reply to this Comment

@László,

That's super interesting. I still can't quite wrap my head around it (since I don't technically understand Observables yet - never looked into it). But, the performance boost looks amazing!

Reply to this Comment

@Dustin,

I love the idea of the bind-once concept. The biggest hurdle for me is that our production app uses a combination of cached data AND live data. So, we use cached data to render once, then (eventually) live data to update the current rendering. This has, to date, made the bind-once approach a struggle.

That said, I'm really putting a lot of effort into optimization these days as the size of the app grows, so hopefully I'll find things like this to help.

Reply to this Comment

@Sekib,

Yes, isolate scope watchers will not be counted here. I did not find any good example which can give watchers attached to isolated scope. Still digging in more.

Reply to this Comment

@All,

I have updated my bookmarklet to account for both normal scopes and isolate scopes:

http://www.bennadel.com/blog/2730-counting-the-number-of-watchers-in-angularjs-updated-for-isolate-scopes.htm

According to the AngularJS 1.3 documentation, however, using the DOM to access to the various scopes may be disabled in production environments (for performance reasons). As such, it may not be possible in AngularJS 1.3 to introspect an app from the outside... but, that's all theory now - I've never even downloaded the 1.3 library yet :D

Reply to this Comment

I was seriously considering seeking another profession until I read this post and created that bookmarklet (just kidding, but seriously). Angular performance issues were driving me mad, and a lot of the tools that are out there are just not that helpful at tracking down issues.

Thank you!!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.