AngularJS relies heavily on watchers ($watch, $watchGroup, and $watchCollection) to track changes in data and update the UI accordingly. However, excessive or unnecessary watchers can lead to performance issues, especially in large applications.
This guide explores how to avoid unnecessary watchers to optimize AngularJS performance.
1. Understanding the Problem: The Digest Cycle and Watchers
How Watchers Work in AngularJS?
AngularJS tracks changes using a mechanism called the digest cycle. It scans all watchers on $scope
, and if any value has changed, it triggers updates to the DOM.
Types of watchers:
$scope.$watch()
→ Watches a specific variable.$scope.$watchCollection()
→ Watches changes in an array or object properties.$scope.$watchGroup()
→ Watches multiple variables together.
2. Identifying Performance Issues Due to Excessive Watchers
How to Check the Number of Active Watchers?
You can check the number of watchers in an AngularJS application using the browser console:
var totalWatchers = (function() {
var count = 0;
var root = angular.element(document).scope();
var countWatchers = function(scope) {
if (scope.$$watchers) {
count += scope.$$watchers.length;
}
if (scope.$$childHead) {
countWatchers(scope.$$childHead);
}
if (scope.$$nextSibling) {
countWatchers(scope.$$nextSibling);
}
};
countWatchers(root);
return count;
})();
console.log('Total Watchers:', totalWatchers);
If the count is very high (>2000), performance issues are likely.
3. Techniques to Avoid Unnecessary Watchers
1️⃣ Use One-Time Binding for Static Data (::
)
Instead of creating watchers for values that don’t change, use one-time binding (::
) to remove unnecessary watchers.
Bad (Creates a Watcher)
<p>{{ user.name }}</p> <!-- This creates a watcher -->
Good (No Watcher After Initial Binding)
<p>{{ ::user.name }}</p> <!-- One-time binding -->
Best for: Static text, labels, or any data that does not change dynamically.
2️⃣ Use ng-if
Instead of ng-show
Bad (Keeps Element in DOM & Watches It)
<div ng-show="isVisible">Content</div>
ng-show
does not remove the element from the DOM.- The scope remains active, keeping watchers.
Good (Removes Element & Watchers)
<div ng-if="isVisible">Content</div>
Best for: Toggling large UI elements dynamically.
3️⃣ Use track by
in ng-repeat
to Optimize DOM Manipulation
Bad (Creates Unnecessary Watchers)
<li ng-repeat="item in items">{{ item.name }}</li>
- This makes AngularJS re-create elements on every digest cycle.
Good (Uses track by
to Reduce Re-renders)
<li ng-repeat="item in items track by item.id">{{ item.name }}</li>
Best for: Large lists where items have unique identifiers.
4️⃣ Use Object.freeze()
to Prevent Unnecessary Watches
If you have a large static object, you can freeze it to prevent AngularJS from tracking changes.
Example:
$scope.config = Object.freeze({
theme: 'dark',
language: 'English'
});
Best for: Configuration objects that don’t change.
5️⃣ Use $watchCollection
Instead of $watch
for Arrays
If you’re watching arrays, use $watchCollection()
instead of $watch()
to reduce performance overhead.
Bad (Watches Each Element in Array)
$scope.$watch('items', function(newVal, oldVal) {
console.log('Items changed!');
}, true);
- The
true
flag enables deep watching, which is expensive.
Good (Watches Only the Array Reference)
$scope.$watchCollection('items', function(newVal, oldVal) {
console.log('Items changed!');
});
Best for: Watching arrays without tracking every element.
6️⃣ Unregister Watchers When Not Needed ($destroy
)
If you have watchers inside controllers or directives, remove them when the scope is destroyed.
Example:
var unbindWatcher = $scope.$watch('user', function(newVal, oldVal) {
console.log('User changed!');
});
// Unregister watcher when leaving the page
$scope.$on('$destroy', function() {
unbindWatcher();
});
Best for: Controllers, directives, and modals.
7️⃣ Use bindOnce
for Performance-Friendly Directives
If you’re using directives, ensure they don’t create unnecessary watchers.
Bad (Creates Watchers on Each Element)
app.directive('showUsername', function() {
return {
template: '<p>{{ user.name }}</p>'
};
});
Good (One-Time Binding)
app.directive('showUsername', function() {
return {
template: '<p>{{ ::user.name }}</p>'
};
});
Best for: Static directive content.