Counting The Number Of Watchers In AngularJS
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.
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:
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.
Want to use code from this post? Check out the license.
Nice blog post (as always). One question though: since the children can point to the parent scope, shouldn't you also track the scopes by $id? I've found another approach here: http://ng.malsup.com/#!/counting-watchers
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.
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.
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 :-)
Have you used AngularJS Batarang? It lets you see which of your watchers is taking the most time, as well as some other useful AngularJS debugging features
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
But, the AngularJS guys are pretty brilliant, so I am excited to see what they do!
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!
I broke this out into its own GitHub repository:
That version uses `document.querySelectorAll()` and does not rely on jQuery at all. It also takes into account the unique IDs of scopes, so as not to double-count watchers.
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
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
Looks like it may result in as much as a 40x performance increase on supported browsers!
Sadly, Object.observe() suport is rather poor on at the moment but once it becomes better it could be a game changer performance-wise.
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!
Theres an project that provides a work around if you are approaching the 2000 thresh hold that attempts to bind better.
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.
@Dustin: Thanks a ton for pointing out to bindonce, great find! Will definitely use it if the need arises.
I just read your article on various profiling and optimization techniques:
... really high quality stuff! Also, I didn't know that Chrome DevTools had snippets :) Something new to dig into!
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!
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.
Looks like Angular 1.3.x natively supports a bindonce-like approach called one-time bindings. Evaluation is delayed until the values in question are other than null or undefined. Well suited for XHRs. Looks pretty sweet!
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.
I have ng-count-watchers code snippet that counts both normal and isolate scopes https://github.com/bahmutov/code-snippets
I have updated my bookmarklet to account for both normal scopes and isolate scopes:
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
Amazing solution for watching app performance
thanks for your time and efforts
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.