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.
watchers 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.
watchers 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.
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.
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
This article was written in collaboration with
kannan.avudai,
felix.foerster,
harikrishna.lingamagunta,
carol.sequeira.