watchWatchers is simple function to analyze the Angular watchers on a page.
Run it in the console in the Chrome Developer Tools (alternatively, you can also add it as a snippet).
The original function is available on stackoverflow.

Understanding Angular watchers

In order to understand how `watchWatchers` works and how to read its report, it is important to have a good understanding of how Angular `watcher`s are created.
The following chapters will detail the process to create new `watcher`s and how `watcher`s are stored in the Angular scope.


$watch

The $watch method is key to how data binding works in Angular. $watch can be called directly from an Angular scope object (e.g.: within an Angular controller or directive), but is also used internally by Angular itself. One example would be the $interpolate service, used whenever a binding on a page is created using the standard interpolation {{ }} symbols.

$watch takes three arguments: watchExpression, listener and the optional objectEquality. watchExpression is the expression to be watched, it can be a string (e.g.: an Angular expression) or a function. Whenever the value of watchExpression changes, the listener function is called. When objectEquality is true the values of watchExpression are compared using angular.equals instead of standard strict comparison operators.

If you must create a watcher, always remember to unbind it at the first available opportunity. You can unbind a watcher by calling the unbinding function returned by $watch.

var watchUnbinder = scope.$watch('scopeValueToBeWatcher', function(newVal, oldVal){});
watchUnbinder();

The watchExpression is parsed using the Angular $parse service to convert Angular expressions into functions [1].

Depending on the type of expression, $parse adds a $$watchDelegate [2] to the resulting parsed expression. If a $$watchDelegate is present $watch will use it to create the watcher [3]. As an example, see how oneTimeWatchDelegate [4] watches expressions and then unwatches them once they are set for the first time.

[1] https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L393

[2] https://github.com/angular/angular.js/blob/master/src/ng/parse.js#L1963

[3] https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L396

[4] https://github.com/angular/angular.js/blob/master/src/ng/parse.js#L2079


$digest
and $apply

The scope.$apply function takes a function as parameter which is executed, and after that $scope.$digest is called internally. scope.$apply is a powerful tool that allows you to introduce values from outside Angular into your application. It is fired under the hood by angular on all of its events (ng-click, ng-mousedown etc). The problem arises in the fact that scope.$apply starts at $rootScope and walks the entire scope chain causing every scope to fire every watcher.

scope.$digest on the other hand starts at the specific scope calling it, and only walks down from there hence the performance benefit. The trade off, of course, is that any parent scopes will not receive this update until the next digest cycle.

$digest cycle is needed in few cases within our application:

  • events user interactions triggers application state change
  • $http requests to get data from the server and update the model accordingly
  • $timeout/$interval – Asynchronous operations cause through timers that can possibly change the state of the application


$destroy

One should always explicitly call $on(‘$destroy’), unbind all watchers and event listeners and cancel any instances of $timeout, or other asynchronous ongoing interactions. It removes the current scope (and all of its children) from the parent scope and marks it for garbage collection instead of having them running in the background occupying all that memory thus leading to memory leaks.


watcher
 object structure

Angular watchers are stored in the $$watchers property of the scope object [1] as objects with five properties: eq, exp, fn, get and last.
Below is a sample watcher from an actual Angular application (BUILD) as it looks at runtime.

{
eq: false
exp: "!!propertyPanel.widthProperty && !!propertyPanel.heightProperty"
fn: function ngIfWatchAction(value)
get: function $parseBinaryFn(self, locals)
last: false
}

In the following paragraphs, we will detail the meaning of each property, this will help us to understand how watchWatchers performs its analysis.


exp

exp is the expression being watched. Please notice that this value could be in turn:

  • a string, in this case it looks like the example above and corresponds to the expression that we actually wrote in the code;
  • a function as passed directly to $watch;
  • a function created by the $$watchDelegate. In this case, the original expression was a string but has been converted into functions by the $parse service.


fn

The listener passed to $watch. Note that in some scenarios, this could be a pointer to the Angular noop function.
This might sound counterintuitive, but it is quite frequently present in the Angular source code. The Angular $location service [2], uses an undefined listener as it calls $evalAsync in the function passed as watchExpression. The Angular $compile service also uses a similar technique [3] when setting up watches for directive bindings.


eq

Is set to the value of the objectEquality argument.


get

Is the function returned by the $parse service. Notice that if the $parse service sets a $$watchDelegate then get and exp will have the same value.


last

The previous value of the watchExpression.

[1] https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L400

[2] https://github.com/angular/angular.js/blob/master/src/ng/location.js#L964

[3] https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L3442

Understanding the watchWatchers function

watchWatchers recursively searches every scope object starting at the $rootScope to collect the list of watchers in the application.
Based on the structure of the watcher object and a little bit of introspection, we provide an analysis of such objects.
In particular, the watchWatchers function reports:

  • the total number of watchers;
  • the list of watchers grouped by the name of the listener function (fn);
  • the list of watchers grouped by the watch expression (only if it’s a string);
  • a set of warnings to possibly help to reduce the amount of watchers.

Total number of `watcher`s

This can be used in basic performance analysis to get an idea of the complexity of every $digest loop, knowing that a higher number of watchers could result in a slower $digest loop.


watcher
s grouped by listener function name

This can be used to get a general idea of which directives or services are creating the watchers.
This is made possible by the fact that most of the Angular source code uses named functions when creating watchers. For example if the name of the listener function is ngClassWatchAction, we can easily infer that such watcher has been created by an instance of the ng-class directive [1]. By grouping watchers whose listener function is ngClassWatchAction we can easily count how many instances of the ng-class directive are used.


watcher
s grouped by expression

This can be used to find patterns of expressions watched multiple times, these could be potential candidates for one-time binding.

Rules

A list of “rules” are defined and used to parse the list of watchers. The rules currently defined by watchWatchers are described below.

High number of ng-value

This rule looks for watchers whose listener function is named valueWatchAction used by the ng-value directive [1]. If a high number of watchers created by ng-value is detected, watchWatchers recommends to verify if such bindings can be converted to one-time bindings.

Watchers to undefined expressions

This is a symptom of a possible bug in the code. If we are watching an expression that is undefined its listener will never be called. In this case watchWatchers recommends to check if the watched expression is defined before attaching a watcher to it.

High number of watchers to the same string expressions

The same value being watched multiple times could be symptom of a binding that should/could be turned into a one-time binding. This is common for bindings created with ng-repeat.

High number of ng-show/ng-hide

If a high number of ng-show/ng-hide directives is detected, watchWatchers recommends to check if they can be turned into ng-ifs. The ng-if removes the actual DOM node while ng-show/ng-hide simply hides it using CSS. Anything shown or hidden will still be on the page, albeit invisible. Any scopes will exist, all $$watchers will fire. Anything removed with ng-if will have no scope. Although this should be a case by case situation.The questions that need to be answered to make this decision are:

  • How frequently will this change? (the more frequent, the worse fit ng-if is).
  • How heavy is the scope/DOM structure ? (the heavier, the better fit ng-if is).

[1] https://github.com/angular/angular.js/blob/master/src/ng/directive/ngClass.js#L17

[2] https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L2097

Find below on how to improve performance of large AngularJS applications reduce watchers and optimize digest cycle

  • https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications
  • http://www.alexkras.com/11-tips-to-improve-angularjs-performance/

This article was written in collaboration with@kannan.avudai,@felix.foerster@harikrishna.lingamagunta@carol.sequeira.

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply