Memory leak due to unhandled $watch

Loading

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()

  1. Check $scope.$$watchers.length console.log($scope.$$watchers.length);
    • If the number keeps increasing, watches are not being removed.
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *