In this Lesson, we will cover the following recipes:
$watch
types efficiently$watch
$watch
$watchCollection
$watch
deregistrationng-repeat
ng-repeat
As with most technologies, in AngularJS, the devil is in the details.
In general, the lion's share of encounters with AngularJS's sluggishness is a result of overloading the application's data-binding bandwidth. Doing so is quite easy, and a normative production application contains a substantial amount of data binding, which makes architecting a snappy application all the more difficult. Thankfully, for all the difficulties and snags that one can encounter involving scaled data binding, the use of regimented best practices and gaining an appreciation of the underlying framework structure will allow you to effectively circumnavigate performance pitfalls.
Implementation of configurations and combinations that lead to severe performance degradation is often difficult to pinpoint as the contributing components by themselves often appear to be totally innocuous.
The following scenarios are just a handful of the commonly encountered scenarios that degrade the application's performance and responsiveness.
Filters will be executed every single time the enumerable collection detects a change, as shown here:
<div ng-repeat="val in values | filter:slowFilter"></div>
Building and using filters that require a great deal of processing is not advisable as you must assume that filters will be called a huge number of times throughout the life of the application.
You might find it tempting to create a scope watcher that evaluates the entirety of a model object; this is accomplished by passing in true
as the final argument, as shown here:
$scope.$watch(giganticObject, function() { ... }, true);
This is a poor design decision as AngularJS needs to be able to determine whether or not the object has changed between $digest
cycles, which of course means storing a history of the object's exact value, as well as exhaustively comparing it each time.
Although it is extremely convenient in a number of scenarios, $watchCollection
can trap you if you try to locate the index of change within it. Consider the following code:
$scope.$watchCollection(giganticArray, function(newVal, oldVal, scope) { var count = 0; // iterate through newVal array angular.forEach(newVal, function(oldVal) { // if the array snapshot index doesn't match, // this implies a change in model value if (newVal[count] !== oldVal[count]) { // logic for matched object delta } count++; }); });
In every $digest
cycle, the watcher will iterate through each watched array in order to find the index/indices that have changed. Since this watcher is expected to be invoked quite often, this approach has the potential to introduce performance-related problems as the watched collection grows.
Each bound expression in a template will register its own watch list entry in order to keep the data fully bound to the view. Suppose that you were working with data in a 2D grid, as follows:
<div ng-repeat="row in rows"> <div ng-repeat="val in row"> {{ val }} </div> </div>
Assuming that rows
is an array of arrays, this template fragment creates a watcher for every individual element in the 2D array. Since watch lists are processed linearly, this approach obviously has the potential to severely degrade the application's performance.
These are only a handful of scenarios that can cause problems for your application. There is a virtually unlimited number of possible configurations that can cause an unexpected slowdown in your application, but being vigilant and watching out for common performance anti-patterns will ameliorate much of the headache that comes along with debugging the slowness of an application.
Since a multiplicity of AngularJS watchers is so commonly the root cause of performance problems, it is quite valuable to be able to monitor your application's watch list and activity. Few beginner level AngularJS developers realize just how often the framework is doing the dirty checking for them, and having a tool that gives them direct insight into when the framework is spending time to perform model history comparisons can be extremely useful.
The $scope.$watch()
, $scope.$watchGroup()
, and $scope.$watchCollection()
methods are normally keyed with a stringified object path, which becomes the target of the change listener. However, if you wish to register a callback for any watch callback irrespective of the change listener target, you can decline to provide a change listener target, as follows:
// invoked once every time $scope.foo is modified $scope.$watch('foo', function(newVal, oldVal, scope) { // newVal is the current value of $scope.foo // oldVal is the previous value of $scope.foo // scope === $scope }); // invoked once every time $scope.bar is modified $scope.$watch('bar', function(newVal, oldVal, scope) { // newVal is the current value of $scope.bar // oldVal is the previous value of $scope.bar // scope === $scope }); // invoked once every $digest cycle $scope.$watch(function(scope) { // scope === $scope });
JSFiddle: http://jsfiddle.net/msfrisbie/r36ak6my/
There's no trickery here; the universal watcher is a feature that is explicitly provided by AngularJS. Although it invokes $watch()
on a scope object, the callback will be executed for every model's modification, independent of the scope upon which it is defined.
Although the watch callback will occur for model modifications anywhere, the lone scope
parameter for the callback will always be the scope upon which the watcher was defined, not the scope in which the modification occurred.
The Batarang browser plugin allows you to inspect the application's watch tree, but there are many scenarios where dynamically inspecting the watch list within the console or application code can be more helpful when debugging or making design decisions.
The following function can be used to inspect all or part of the DOM for watchers. It accepts an optional DOM element as an argument.
var getWatchers = function (element) { // convert to a jqLite/jQuery element // angular.element is idempotent var el = angular.element( // defaults to the body element element || document.getElementsByTagName('body') ) // extract the DOM element data , elData = el.data() // initalize returned watchers array , watchers = []; // AngularJS lists watches in 3 categories // each contains an independent watch list angular.forEach([ // general inherited scope elData.$scope, // isolate scope attached to templated directive elData.$isolateScope, // isolate scope attached to templateless directive elData.$isolateScopeNoTemplate ], function (scope) { // each element may not have a scope class attached if (scope) { // attach the watch list watchers = watchers.concat(scope.$$watchers || []); } } ); // recurse through DOM tree angular.forEach(el.children(), function (childEl) { watchers = watchers.concat(getWatchers(childEl)); }); return watchers; };
JSFiddle: http://jsfiddle.net/msfrisbie/d58g77m1/
With this, you are able to call the function with a DOM node and ascertain which watchers exist inside it, as follows:
// all watchers in the document getWatchers(document); // all watchers in the signup form with a selector getWatchers(document.getElementById('signup-form')); // all watchers in <div class="container"></div> getWatchers($('div.container'));
It is possible to access a DOM element's $scope
object (without injecting it) through the jQuery/jqLite element object's data()
method. The $scope
object has a $$watchers
property that lists how many watchers are actively defined upon that $scope
object.
The preceding function exhaustively recurses through the DOM tree and inspects each node in order to determine whether it has a scope attached to it. If it does, any watchers defined on that scope are read and entered into the master watch list.
This is only a single, general implementation of watcher inspection. Since watchers are localized to a single scope, it might behoove you to utilize components of this function in order to inspect single scope instances instead of the child DOM subtree.
The beast behind AngularJS's data binding is its dirty checking and the overhead that comes along with it. As you tease apart your application's innards, you will find that even the most elegantly architected applications incur a substantial amount of dirty checking. This, of course, is normal, and the framework is architected as to be able to handle the hugely variable loads of dirty checking that different sorts of applications might throw at it. Nevertheless, the nature of object comparison performance at scale (hint—it is slow) requires that dirty checking is minimally deployed, efficiently organized, and appropriately targeted. Even with the rigorous engineering and optimization behind AngularJS's dirty checking, it remains the case that it is still deceptively easy to bog down an application's performance with superfluous data comparison. In the same way that a single uncooperative person backpaddling in a canoe can bring a vessel to a halt, a single careless watch statement can bring an AngularJS application's responsiveness to its knees.
Strategies to deploy watchers efficiently can be summed up as follows.
Watchers check the portion of the model they are bound to extremely frequently. If a change in a piece of the model does not affect what the watch callback does, then the watcher shouldn't need to worry about it.
The watch expression $scope.$watch('
myWatchExpression
', function() {});
will be evaluated in every digest cycle in order to determine the output. You'll be able to put expressions such as 3 + 6
or myFunc()
as the expression, but these will be evaluated in every single digest cycle in an effort to obtain a fresh return value in order to compare it against the last recorded return value. Very rarely is this necessary, so stick to binding watchers to model properties.
It stands to reason that, as the entire watch list must be evaluated in every $digest
cycle, fewer watchers in that list will yield a speedier $digest
cycle.
Reference watches register a listener that uses strict equality (===
) as the comparator, which verifies the congruent object identity or primitive equality. The implication of this is that a change will only be registered if the model the watcher is listening to is assigned to a new object.
The reference watcher should be used when the object's properties are unimportant. It is the most efficient of the $watch
types as it only demands top-level object comparison.
The watcher can be created as follows:
$scope.myObj = { myPrim: 'Go Bears!', myArr: [3,1,4,1,5,9] }; // watch myObj by reference $scope.$watch('myObj', function(newVal, oldVal, scope) { // callback logic }); // watch only the myPrim property of myObj by reference $scope.$watch('myObj.myPrim', function(newVal, oldVal, scope) { // callback logic }); // watch only the second element of myObj.myArr by reference $scope.$watch('myObj.myArr[1]', function(newVal, oldVal, scope) { // callback logic });
The reference comparator will only invoke the watch callback upon object reassignment.
Suppose that a $scope
object was initialized as follows:
$scope.myObj = { myPrim: 'Go Bears!' }; $scope.myArr = [3,1,4,1,5,9]; // watch myObj by reference $scope.$watch('myObj', function() { // callback logic }); // watch myArr by reference $scope.$watch('myArr', function() { // callback logic });
Any assignment of the watched object to a different primitive or object will register as dirty. The following examples will cause a callback to execute:
$scope.myArr = []; $scope.myObj = 1; $scope.myObj = {};
Beneath the top-level reference watching, any changes that affect the inside of the object will not register as changes. This includes modification, creation, and deletion. The following will not cause the callback to execute:
// replace existing property $scope.myObj.myPrim = 'Go Giants!'; // add new property $scope.myObj.newProp = {}; // push onto array $scope.myArr.push(2); // modify element of array $scope.myArr[0] = 6; // delete property delete myObj.myPrim;
JSFiddle: http://jsfiddle.net/msfrisbie/h7hvbfkg/
The long and short of it is that reference watchers are the most efficient type of watchers, so when you are looking to set up a watcher, reach for this one first.
Equality watches register a listener that uses angular.equals()
as the comparator, which exhaustively examines the entirety of all objects to ensure that their respective object hierarchies are identical. Both a new object assignment and property modification will register as a change and invoke the watch callback.
This watcher should be used when any modification to an object is considered as a change event, such as a user object having its properties at various depths modified.
The equality comparator is used when the optional Boolean third argument is set to true
. Other than that, these watchers are syntactically identical to reference comparator watchers, as shown here:
$scope.myObj = {
myPrim: 'Go Bears!',
myArr: [3,1,4,1,5,9]
};
// watch myObj by equality
$scope.$watch('myObj', function(newVal, oldVal, scope) {
// callback logic
}, true);
The equality comparator will invoke the watch callback on every modification anywhere on or inside the watched object.
Suppose that a $scope
object is initialized as follows:
$scope.myObj = { myPrim: 'Go Bears!' }; $scope.myArr = [3,1,4,1,5,9]; // watch myObj by equality $scope.$watch('myObj', function() { // callback logic }, true); // watch myArr by equality $scope.$watch('myArr', function() { // callback logic }, true);
All of the following examples will cause a callback to be executed:
$scope.myArr = []; $scope.myObj = 1; $scope.myObj = {}; $scope.myObj.myPrim = 'Go Giants!'; $scope.myObj.newProp = {}; $scope.myArr.push(2); $scope.myArr[0] = 6; delete myObj.myPrim;
JSFiddle: http://jsfiddle.net/msfrisbie/w24mrkfm/
Since a watcher must store the past version of the watched object to compare against it and perform the actual comparison, equality watchers utilize both the angular.copy()
method to store the object and the angular.equals()
method to test the equality. For large objects, it is not difficult to discern that these operations will introduce latency into the application. Equality comparator watchers should not be used unless absolutely necessary.
AngularJS offers the $watchCollection
intermediate watch type to register a listener that utilizes a shallow watch depth for comparison. The $watchCollection
type will register a change event when any of the object's properties are modified, but it is unconcerned with what those properties refer to.
This watcher is best used with arrays or flat objects that undergo frequent top-level property modifications or reassignments. Currently, it does not provide the modified property(s) responsible for the callback, only the entire objects, so the callback is responsible for determining which properties or indices are incongruent. This can be done as follows:
$scope.myObj = { myPrimitive: 'Go Bears!', myArray: [3,1,4,1,5,9] }; // watch myObj and all top-level properties by reference $scope.$watchCollection('myObj', function(newVal, oldVal, scope) { // callback logic }); // watch myObj.myArr and all its elements by reference $scope.$watchCollection('myObj.myArr', function(newVal, oldVal, scope) { // callback logic });
The $watchCollection
utility will set up reference watchers on the model object and all its existing properties. This will invoke the watch callback upon object reassignment or upon top-level property reassignment.
Suppose that a $scope
object is initialized as follows:
$scope.myObj = { myPrim: 'Go Bears!', innerObj: { innerProp: 'Go Bulls!' } }; $scope.myArr = [3,1,4,1,5,9]; // watch myObj as a collection $scope.$watchCollection('myObj', function() { // callback logic }); // watch myArr as a collection $scope.$watchCollection('myArr', function() { // callback logic });
The following examples will cause a callback to be executed:
// object reassignment $scope.myArr = []; $scope.myObj = 1; $scope.myObj = {}; // top-level property reassignment $scope.myObj.myPrim = 'Go Giants!'; // array element reassignment $scope.myArr[0] = 6; // deletion of top level property delete myObj.myPrim;
The following will not cause the callback to be executed:
// add new property $scope.myObj.newProp = {}; // push new element onto array $scope.myArr.push(2); // modify, create, or delete nested property $scope.myObj.innerObj.innerProp = 'Go Blackhawks!'; $scope.myObj.innerObj.otherProp = 'Go Sox!'; delete $scope.myObj.innerObj.innerProp;
JSFiddle: http://jsfiddle.net/msfrisbie/jnL12sck/
The name $watchCollection
is a bit deceptive (depending on how you think about enumerable collections in JavaScript) as it might not perform how you would expect—especially since it doesn't watch for elements that are being added to the collection. Since explicitly-defined properties and array indices are effectively identical at the object property level, $watchCollection
is really more of a single-depth reference watcher.
Nothing boosts watcher performance quite like destroying the watcher altogether. Should you encounter a scenario where you no longer have a need to watch a model component, invoking watch creation returns a deregistration function that will unbind that watcher when called.
When a watcher is initialized, it will return its deregistration function. You must store this deregistration function until it needs to be invoked. This can be done as follows:
$scope.myObj = {} // watch myObj by reference var deregister = $scope.$watch('myObj', function(newVal, oldVal, scope) { // callback logic }); // prevent additional modifications from invoking the callback deregister();
JSFiddle: http://jsfiddle.net/msfrisbie/yLhwfvwL/
The $watch
destruction will normally be needed when a change in application state causes a watch to no longer be useful while the scope that it is defined inside still exists. When a scope is destroyed—either manually or automatically—the watchers defined upon it will be flagged as eligible for garbage collection, and therefore, manual teardown is not required.
However, this is contingent upon the scope on which the watcher is destroyed. If your application has watchers defined on a parent scope or $rootScope
, they will not be flagged for garbage collection and must be destroyed manually upon scope destruction (usually accomplished with $scope.$on('$destroy', function() {})
), or else your application is subject to potential memory leaks in the form of orphaned watchers.
Any AngularJS template expression inside double braces ({{ }}
) will register an equality watcher using the enclosed AngularJS expression upon compilation.
Curly braces are easily recognized as the AngularJS syntax for template data binding. The following is an example:
<div ng-show="{{myFunc()}}"> {{ myObj }} </div>
On a high level, even to a beginner level AngularJS developer, this is painfully obvious.
Interpolating the two preceding expressions into the view implicitly creates two watchers for each of these expressions. The corresponding watchers will be approximately equivalent to the following:
$scope.$watch('myFunc()', function() { ... }, true); $scope.$watch('myObj', function() { ... }, true);
The AngularJS expression contained within {{ }}
in the template will be the exact entry registered in the watch list. Any method or logic within that expression will necessarily be evaluated for its return value every time dirty checking is performed. An observant developer will note that any logic contained in myFunc()
will be evaluated on every single digest cycle, which can degrade the performance extremely rapidly. Therefore, it will benefit your application greatly to have the value of the watch entry calculable as quickly as possible. An easy way to accomplish this is to not provide methods or logic as expressions at all, but to calculate the output of the method and store it in a model property, which can then be passed to the template.
Template watch entries have setup and teardown processes automatically taken care of for you. You must be careful though, as using {{ }}
in your template will sneakily cause your watch count to balloon. AngularJS 1.3 introduces bind once capabilities, which allow you to interpolate model data into the view upon compilation, but not to bring along the overhead of data binding, if it will not be necessary.
An extremely common pattern in an AngularJS application is to have an ng-repeat
directive instance spit out a list of child directives corresponding to an enumerable collection. This pattern can obviously lead to performance problems at scale, especially as directive complexity increases. One of the best ways to curb directive processing bloat is to eliminate any processing redundancy by migrating it to the compile phase.
Suppose that your application contains the following pseudo-setup. This is what we need for the next section:
(index.html) <div ng-repeat="element in largeCollection"> <span my-directive></span> </div> (app.js) angular.module('myApp', []) .directive('myDirective', function() { return { link: function(scope, el, attrs) { // general directive logic and initialization // instance-specific logic and initialization } }; });
A clever developer will note that since a directive's link
function executes once for each instance of the directive in the repeater, the current implementation is wasting time performing the same actions for each instance.
Since the compile phase will only occur once for all directives inside an ng-repeat
directive, it makes sense to perform all generalized logic and initialization within that phase, and share the results with the returned link
function. This can be done as follows:
(app.js) angular.module('myApp', []) .directive('myDirective', function() { return { compile: function(el, attrs) { // general directive logic and initialization return function link(scope, el, attrs) { // instance-specific logic and initialization // link function closure can access compile vars }; } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/mopuxn8h/
The ng-repeat
directive will implicitly reuse the same compile
function for all the directive instances it creates. Therefore, it's a no-brainer that any redundant processing done inside link
functions should be moved to the compile
function as far as possible.
This is by no means a fix all for the sluggishness of ng-repeat
, as high latency can stem from a large number of common problems when iterating through huge amounts of bound data. However, using the compile phase effectively is an often overlooked strategy that has the potential to yield huge performance gains from a relatively simple refactoring.
Furthermore, even though this condenses logic into a single compile phase per ng-repeat
, the compile logic will still get executed once for every instance of the directive in the template. If you truly want the logic to only get executed once for the entire application, use the fact that service types are singletons to your advantage, and migrate the logic inside one of them.
By default, ng-repeat
creates a DOM node for each item in the collection and destroys that DOM node when the item is removed. It is often the case that this is suboptimal for your application's performance, as a constant stream of re-rendering a sizeable collection will rarely be necessary at the repeater level and will tax your application's performance heavily. The solution is to utilize the track by
expression, which allows you to define how AngularJS associates DOM nodes with the elements of the collection.
When track by $index
is used as an addendum to the repeat expression, AngularJS will reuse any existing DOM nodes instead of re-rendering them.
The original, suboptimal version is as follows:
<div ng-repeat="element in largeCollection"> <!-- element repeater content --> </div>
The optimized version is as follows:
<div ng-repeat="element in largeCollection track by $index">
<!-- element repeater content -->
</div>
JSFiddle: http://jsfiddle.net/msfrisbie/0dbj5rgt/
By default, ng-repeat
associates each collection element by reference to a DOM node. Using the track by
expression allows you to customize what that association is referencing instead of the collection element itself. If the element is an object with a unique ID, that is suitable. Otherwise, each repeated element is provided with $index
on its scope, which can be used to uniquely identify that element to the repeater. By doing this, the repeater will not destroy the DOM node unless the index changes.
The equality comparator watcher can be a fickle beast when tuning the application for better performance. It's always best to avoid it when possible, but of course, that holds true until you actually need to deep watch a collection of large objects. The overhead of watching a large object is so cumbersome that sometimes distilling objects down to a subset for the purposes of comparison can actually yield performance gains.
The following is the naïve method of an exhaustive equality comparator watch:
$scope.$watch('bigObjectArray', function() { // watch callback }, true);
Instead of watching the entire object, it is possible to call map()
on a collection of large objects in order to extract only the components of the objects that actually need to be watched. This can be done as follows:
$scope.$watch( // function that returns object to be watched function($scope) { // map the array to distill the relevant properties // this return value is what will be compared against return $scope.bigObjectArray.map(function(bigObject) { // return only the property we want return bigObject.relevantProperty; }); }, function(newVal, oldVal, scope) { // watch callback }, // equality comparator true );
JSFiddle: http://jsfiddle.net/msfrisbie/p45jb4dh/
The $watch
expression can be passed anything that it can compare to a past value; it does not have to be an AngularJS string expression. The outer function is evaluated for its return value, which is used as the value to compare against. For each cycle, the dirty checking mechanism will map the array, test it against the old value, and record the new value.
If the time it takes to copy and compare the entire object array is greater than the time it takes to use map()
on the array and compare the subsets, then using the watcher in this way will yield a performance boost.
35.171.45.182