In AngularJS, the $digest
cycle is responsible for detecting changes and updating the DOM accordingly. However, frequent triggering of the $digest
cycle (especially on high-frequency events like mousemove
, scroll
, and keypress
) can severely impact performance.
This guide covers:
Understanding the $digest
cycle
Why frequent $digest
cycles are a problem
Methods to prevent unnecessary $digest
triggers
Best practices for optimizing AngularJS performance
1. Understanding the $digest
Cycle
AngularJS uses a two-way data binding system, where changes in the model automatically update the view and vice versa. This synchronization happens through a digest cycle, which:
- Scans all
$scope
variables - Checks for changes
- Updates the DOM if needed
Whenever AngularJS detects a change (e.g., button click, API response, event listener), it triggers a new $digest
cycle.
2. Why Frequent $digest
Cycles Cause Performance Issues
Certain events, like mousemove
and scroll
, fire continuously while the user interacts with the page. If AngularJS triggers a $digest
cycle every time, the app can experience:
- Laggy UI
- Unnecessary re-renders
- Increased CPU usage
- Slower performance on low-end devices
Thus, we need to prevent unnecessary digest cycles while still handling events efficiently.
3. Ways to Prevent Unnecessary $digest
Cycles
A. Use $scope.$applyAsync()
Instead of $apply()
Normally, event handlers use $scope.$apply()
, which immediately triggers a digest cycle. Instead, using $scope.$applyAsync()
delays execution until the current cycle is complete.
Example: Using $applyAsync()
to batch updates
app.controller("MainCtrl", function($scope) {
document.addEventListener("mousemove", function(event) {
$scope.$applyAsync(function() {
$scope.mouseX = event.clientX;
$scope.mouseY = event.clientY;
});
});
});
Why?
- Prevents multiple
$digest
cycles from running unnecessarily - Groups multiple updates into one efficient cycle
B. Use $timeout
Instead of $apply()
for Asynchronous Tasks
If an event updates the UI frequently, wrapping it in $timeout
avoids forcing immediate digest cycles.
Example: Using $timeout
to reduce digest cycles
app.controller("MainCtrl", function($scope, $timeout) {
document.addEventListener("scroll", function() {
$timeout(function() {
$scope.scrollPosition = window.scrollY;
}, 200); // Updates at most every 200ms
});
});
Why?
- The
$digest
cycle only runs after 200ms, reducing strain on the system - The UI updates at a controlled frequency
C. Use $evalAsync()
for Deferred Execution
If you want to schedule a digest cycle but allow other tasks to finish first, $evalAsync()
is useful.
Example: Deferring Execution with $evalAsync()
app.controller("MainCtrl", function($scope) {
document.addEventListener("keydown", function(event) {
$scope.$evalAsync(function() {
$scope.lastKeyPressed = event.key;
});
});
});
Why?
- Prevents immediate
$digest
cycles for every keystroke - Ensures updates don’t block other tasks
D. Bind Heavy Event Listeners Outside Angular’s Digest Scope
If an event doesn’t need to update Angular’s data binding, use $scope.$apply()
only when necessary.
Example: Using window.addEventListener
Without Triggering $digest
app.controller("MainCtrl", function($scope) {
document.addEventListener("mousemove", function(event) {
console.log("Mouse moved:", event.clientX, event.clientY);
// No need for $apply since this doesn't update Angular scope
});
});
Why?
- Prevents unnecessary digest cycles for non-Angular logic
- Reduces CPU-intensive tasks
E. Use $scope.$on('$destroy')
to Clean Up Listeners
When controllers or directives are destroyed, clean up event listeners to prevent memory leaks.
Example: Cleaning Up Listeners on Scope Destroy
app.controller("MainCtrl", function($scope) {
function handleScroll() {
console.log("Scrolling:", window.scrollY);
}
window.addEventListener("scroll", handleScroll);
$scope.$on("$destroy", function() {
window.removeEventListener("scroll", handleScroll);
});
});
Why?
- Prevents unused event listeners from keeping memory occupied
- Improves performance in single-page applications (SPA)
F. Use track by
in ng-repeat
to Reduce Digest Overhead
When looping through large datasets, AngularJS creates a new $digest
cycle for every item. Use track by
to reduce unnecessary cycles.
Example: Optimizing ng-repeat
with track by
<ul>
<li ng-repeat="item in items track by item.id">{{ item.name }}</li>
</ul>
Why?
- Prevents Angular from re-checking unchanged list items
- Greatly improves performance in large lists
4. Best Practices for Reducing $digest
Cycle Execution Time
✔️ Use $scope.$applyAsync()
instead of $apply()
✔️ Use $timeout()
to throttle high-frequency updates
✔️ Bind event listeners outside of AngularJS when possible
✔️ Destroy event listeners using $scope.$on('$destroy')
✔️ Optimize ng-repeat
with track by
✔️ Use $evalAsync()
for deferred execution
✔️ Avoid deep watching large objects ($watchCollection()
instead of $watch()
)