In AngularJS, $watch()
is used to monitor scope variables and trigger a function when the value changes. However, if $watch()
is not properly cleaned up, it can cause memory leaks, especially in long-running applications.
Step 1: Understanding How $watch()
Causes Memory Leaks
How $watch()
Works
$scope.$watch('variableName', function(newValue, oldValue) {
console.log('Value changed from', oldValue, 'to', newValue);
});
- AngularJS continuously checks the variable inside the digest cycle.
- If the variable is no longer used but
$watch()
is still running, it consumes memory.
Common Scenarios That Cause Memory Leaks
1. $watch()
Inside a Controller Without Cleanup
If you create multiple watches without removing them, they stay active even when the scope is destroyed.
Example (Memory Leak)
app.controller('MainCtrl', function($scope) {
$scope.counter = 0;
$scope.$watch('counter', function(newValue) {
console.log('Counter changed:', newValue);
});
});
- If the controller is reloaded or used in a single-page app, old
$watch()
instances remain active. - This can slow down the application over time.
Solution: Unregister $watch()
When the Scope is Destroyed
app.controller('MainCtrl', function($scope) {
$scope.counter = 0;
var unwatch = $scope.$watch('counter', function(newValue) {
console.log('Counter changed:', newValue);
});
$scope.$on('$destroy', function() {
unwatch(); // Cleanup the watch
});
});
This removes the watch when the scope is destroyed, preventing memory leaks.
2. $watch()
Inside a Directive Without Cleanup
If $watch()
is added inside a directive without proper cleanup, it stays active even if the element is removed.
Example (Memory Leak)
app.directive('customDirective', function() {
return {
restrict: 'E',
scope: { value: '=' },
link: function(scope) {
scope.$watch('value', function(newValue) {
console.log('Directive watch:', newValue);
});
}
};
});
- If this directive is removed from the DOM,
$watch()
still runs.
Solution: Cleanup $watch()
When Element Is Removed
app.directive('customDirective', function() {
return {
restrict: 'E',
scope: { value: '=' },
link: function(scope, element) {
var unwatch = scope.$watch('value', function(newValue) {
console.log('Directive watch:', newValue);
});
element.on('$destroy', function() {
unwatch(); // Remove watch when directive is removed
});
}
};
});
3. Watching Large Data Structures
If $watch()
tracks a large array or object, it keeps checking every element even if you don’t use it anymore.
Example (Memory Leak)
$scope.$watch('largeArray', function(newValue) {
console.log('Array changed:', newValue);
});
This forces deep comparison, making digest cycles slower over time.
Solution: Use $watchCollection()
or a Shallow Watch
$scope.$watchCollection('largeArray', function(newValue) {
console.log('Array changed:', newValue);
});
or manually track only needed values:
$scope.$watch(function() {
return $scope.largeArray.length;
}, function(newLength) {
console.log('Array length changed:', newLength);
});
4. $watch()
Inside a $timeout
or $interval
If $watch()
is inside $timeout
or $interval
without being canceled, it keeps running indefinitely, even when not needed.
Example (Memory Leak)
$timeout(function() {
$scope.$watch('variable', function(newValue) {
console.log('Timeout watch:', newValue);
});
}, 5000);
Solution: Use $timeout.cancel()
or $interval.cancel()
var timeoutPromise = $timeout(function() {
var unwatch = $scope.$watch('variable', function(newValue) {
console.log('Timeout watch:', newValue);
});
$scope.$on('$destroy', function() {
unwatch(); // Cleanup watch
});
}, 5000);
$scope.$on('$destroy', function() {
$timeout.cancel(timeoutPromise); // Cleanup timeout
});
Step 2: Debugging and Fixing Memory Leaks
How to Detect Memory Leaks in $watch()
- Check
$scope.$$watchers.length
console.log($scope.$$watchers.length);
- If the number keeps increasing, watches are not being removed.
- Use Chrome DevTools
- Open Memory Tab → Take Heap Snapshots.
- Look for Detached Scope Objects (these indicate uncleaned-up
$watch()
instances).
Step 3: Best Practices to Prevent $watch()
Memory Leaks
Use $scope.$on('$destroy', unwatch)
to clean up watchers.
Prefer $watchCollection()
instead of deep $watch()
for arrays.
Avoid $watch()
inside $timeout
or $interval
without cancellation.
Use Angular’s one-time binding (::
) if data won’t change.
Final Code Example (Safe Usage of $watch()
)
app.controller('SafeController', function($scope, $timeout) {
$scope.value = 0;
var unwatch = $scope.$watch('value', function(newValue) {
console.log('Value changed:', newValue);
});
// Cleanup when the scope is destroyed
$scope.$on('$destroy', function() {
unwatch();
});
// Example of canceling $timeout
var timeoutPromise = $timeout(function() {
console.log('Timeout executed');
}, 5000);
$scope.$on('$destroy', function() {
$timeout.cancel(timeoutPromise);
});
});
Final Thoughts
Memory leaks due to unhandled $watch()
can slow down AngularJS apps over time. The best way to prevent this is to remove watches when they are no longer needed using $destroy
.