In AngularJS, excessive watchers can slow down an application due to frequent digest cycles. Each watcher adds a performance cost because AngularJS continuously checks for changes in watched variables. This guide explains how to reduce unnecessary watchers and optimize performance.
1. Understanding Watchers and the Digest Cycle
AngularJS uses the digest cycle to detect changes in the application. Every $scope.$watch()
, ng-model
, ng-repeat
, ng-if
, and ng-show
directive adds a watcher.
Problem:
- Too many watchers = Longer digest cycles
- UI lag, slow updates, and poor user experience
Example of Watcher Growth:
$scope.count = 0;
$scope.$watch("count", function(newValue, oldValue) {
console.log("Count changed:", newValue);
});
- If thousands of watchers are added, the digest cycle takes longer.
2. How to Identify Excessive Watchers?
Using Chrome DevTools
1️⃣ Open Chrome DevTools (F12
or Ctrl + Shift + I
).
2️⃣ Go to the Console tab.
3️⃣ Run the following command:
angular.element(document).injector().get('$rootScope').$$watchersCount
4️⃣ It returns the total number of watchers in your app.
3. Techniques to Reduce Watchers in AngularJS
1. Use ng-if
Instead of ng-show
ng-show
: Keeps the element in the DOM but toggles visibility (watchers remain active).ng-if
: Removes the element from the DOM when false (removes watchers).
<!-- Inefficient -->
<div ng-show="isVisible">This div always exists in the DOM.</div>
<!-- Efficient -->
<div ng-if="isVisible">This div is removed when not needed.</div>
✔ Better Performance: ng-if
destroys scope watchers when the element is removed.
2. Use one-time binding (::)
for Static Data
If a value does not change, prevent AngularJS from watching it.
<!-- Inefficient -->
<h1>{{ user.name }}</h1> <!-- Always watched -->
<!-- Efficient -->
<h1>{{ ::user.name }}</h1> <!-- One-time binding (not watched) -->
✔ Reduces watchers because AngularJS does not track changes after the first load.
3. Optimize ng-repeat
with track by
ng-repeat
creates a watcher for each item in an array.
Problem:
<!-- Inefficient -->
<div ng-repeat="item in items">
{{ item.name }}
</div>
Solution: Use track by
to improve performance.
<!-- Efficient -->
<div ng-repeat="item in items track by item.id">
{{ item.name }}
</div>
✔ Prevents Angular from re-creating the DOM unnecessarily.
4. Limit the Use of $watch
Avoid unnecessary $watch()
calls in controllers.
Inefficient:
$scope.$watch("data", function(newVal) {
console.log("Data changed!");
}, true); // Deep watch (bad for performance)
Efficient:
$scope.$watchCollection("data", function(newVal) {
console.log("Data changed!");
});
✔ $watchCollection()
only watches first-level changes, reducing processing load.
5. Remove Watchers When No Longer Needed
$watch()
returns a deregistration function. Use it to manually remove watchers.
var unwatch = $scope.$watch("someVar", function(newVal) {
console.log("Value changed:", newVal);
});
// Remove the watcher when done
unwatch();
✔ Useful for cleaning up unused watchers.
6. Use $scope.$evalAsync()
Instead of $apply()
$apply()
triggers a full digest cycle (expensive).$evalAsync()
schedules a digest only if needed.
$scope.$evalAsync(function() {
console.log("Executed with less overhead.");
});
✔ Reduces unnecessary digest cycles.
7. Use Web Workers for Heavy Computation
- Move expensive calculations outside AngularJS scope using Web Workers.
Without Web Worker (Blocks UI)
$scope.processData = function() {
for (let i = 0; i < 1000000000; i++) {
// Expensive computation
}
};
With Web Worker (Better Performance)
var worker = new Worker("worker.js");
worker.postMessage(data);
worker.onmessage = function(event) {
$scope.result = event.data;
};
✔ Keeps the UI responsive.
4. Summary – Best Practices to Reduce Watchers
Technique | Benefit |
---|---|
Use ng-if instead of ng-show | Removes watchers when not needed |
Use :: one-time binding | Prevents Angular from tracking static values |
Use track by in ng-repeat | Avoids unnecessary DOM updates |
Avoid deep $watch() | Use $watchCollection() for better performance |
Manually remove watchers | Deregister watchers when they are no longer needed |
Use $scope.$evalAsync() | Minimizes digest cycle triggers |
Move heavy processing to Web Workers | Keeps UI responsive |