Avoiding excessive $watch usage for performance

Loading

AngularJS’s $watch() is powerful for tracking changes in scope variables, but excessive use can significantly impact performance. Since AngularJS runs a digest cycle to check for changes, having too many watchers can slow down the application, especially for large datasets or complex views.

In this guide, we’ll explore why excessive $watch usage is problematic, and how to optimize performance effectively.


Why Excessive $watch Calls Hurt Performance

  • Every $watch() adds a function to AngularJS’s digest cycle.
  • The more watchers you have, the longer the digest cycle takes.
  • A digest cycle runs multiple times (up to 10 times) if changes are detected.
  • Large lists, deep objects, and unnecessary $watch calls cause UI lag.

Best Practices to Avoid Performance Issues

1️⃣ Use $watchCollection() Instead of $watch() (When Possible)

Why?

  • $watch() with { deep: true } tracks deep changes, which is expensive.
  • $watchCollection() is faster as it only tracks first-level changes.

Example – Using $watchCollection() Instead of Deep $watch()

$scope.items = [{ name: "Apple" }, { name: "Banana" }];

// Inefficient deep watch
$scope.$watch('items', function(newVal, oldVal) {
console.log("Items changed:", newVal);
}, true); // Deep watching enabled (BAD for performance)

// Optimized: Using $watchCollection
$scope.$watchCollection('items', function(newVal, oldVal) {
console.log("Items list changed:", newVal);
});

Performance Boost: $watchCollection() ignores deep changes inside objects and runs faster than deep $watch().


2️⃣ Use $watchGroup() for Multiple Variables

Instead of multiple $watch() calls, use $watchGroup() to combine them into one.

Example – Replacing Multiple $watch() Calls with $watchGroup()

$scope.firstName = "John";
$scope.lastName = "Doe";

// ❌ Inefficient: Multiple $watch() calls
$scope.$watch('firstName', function(newVal) { console.log("First name changed:", newVal); });
$scope.$watch('lastName', function(newVal) { console.log("Last name changed:", newVal); });

// Optimized: Using $watchGroup()
$scope.$watchGroup(['firstName', 'lastName'], function(newValues) {
console.log("Name changed:", newValues);
});

Performance Boost: Reduces the number of $watch() calls, improving digest cycle efficiency.


3️⃣ Avoid Watching Large Arrays – Use track by in ng-repeat

Watching large datasets is expensive. Instead of tracking entire arrays, use track by in ng-repeat to optimize performance.

Example – Using track by to Prevent Extra Watchers

<!-- BAD: ng-repeat creates an individual watcher for each item -->
<li ng-repeat="item in items">{{ item.name }}</li>

<!-- GOOD: Using 'track by' reduces watcher count -->
<li ng-repeat="item in items track by item.id">{{ item.name }}</li>

Performance Boost: track by prevents AngularJS from unnecessarily recreating watchers for unchanged objects.


4️⃣ Use One-Time Binding (::) for Static Data

If a value never changes after initialization, don’t use $watch() at all. Instead, use one-time binding (::).

🔹 Example – Replacing $watch() with One-Time Binding

<!-- BAD: Uses unnecessary two-way binding -->
<p>{{ user.name }}</p>

<!-- GOOD: Uses one-time binding -->
<p>{{ ::user.name }}</p>

Performance Boost: :: prevents AngularJS from tracking changes, reducing watchers.


5️⃣ Manually Trigger Digest with $applyAsync()

Instead of $scope.$watch(), use $scope.$applyAsync() to defer updates and batch multiple changes together.

Example – Using $applyAsync() to Reduce Digest Cycles

// BAD: Triggers a digest cycle immediately
$scope.$watch('data', function(newVal) {
console.log("Data updated:", newVal);
});

// GOOD: Delays execution and batches changes
$scope.$applyAsync(function() {
console.log("Data updated:", $scope.data);
});

Performance Boost: $applyAsync() waits until the current digest cycle is done before executing, reducing redundant cycles.


6️⃣ Use ng-if Instead of ng-show/ng-hide

AngularJS keeps elements alive when using ng-show/ng-hide, but ng-if removes them from the DOM, reducing watchers.

🔹 Example – Replacing ng-show with ng-if

<!-- BAD: Keeps elements in DOM, leading to unnecessary watchers -->
<div ng-show="isVisible">Visible Content</div>

<!-- GOOD: Removes elements, reducing watchers -->
<div ng-if="isVisible">Visible Content</div>

Performance Boost: ng-if reduces unnecessary watchers when elements are not needed.


7️⃣ Remove Unused $watch() Calls When Not Needed

Unnecessary $watch() calls waste resources. If a watch is temporary, remove it using the returned deregistration function.

Example – Deregistering a Watcher

var unwatch = $scope.$watch('data', function(newVal) {
console.log("Data changed:", newVal);
});

// When the watch is no longer needed
unwatch(); // Stops watching 'data'

Performance Boost: Avoids unnecessary watchers after data updates.


Conclusion: Best Practices Recap

Bad Practice Optimized Solution
Using deep $watch() for objectsUse $watchCollection()
Multiple $watch() callsUse $watchGroup()
Watching large arrays in ng-repeatUse track by
Watching static valuesUse one-time binding (::)
Using $scope.$watch() for async updatesUse $applyAsync()
Using ng-show/ng-hide for conditional elementsUse ng-if
Keeping unnecessary watchers activeDeregister $watch() when not needed

Leave a Reply

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