How does AngularJS internally implement dirty checking, and what are the performance implications of having too many watchers?
Updated Apr 28, 2026
Short answer
AngularJS implements dirty checking by maintaining a list of watchers that observe expressions on scopes and comparing their current values with previously stored values during the digest cycle. When a value changes, AngularJS detects the difference and updates the related bindings or triggers listener functions. Too many watchers increase digest cycle time because AngularJS must evaluate every watcher repeatedly, which can make applications slow and unresponsive.
Deep explanation
AngularJS uses a mechanism called dirty checking to keep the application state and the user interface synchronized. Unlike modern frameworks that rely on explicit change tracking, AngularJS periodically checks whether data values have changed.
The main process involves scopes, watchers, and the digest cycle.
How dirty checking works internally
When AngularJS creates data bindings, directives, or $watch() expressions, it registers watchers on the current scope.
A watcher contains:
- The expression or function to evaluate.
- The last known value of that expression.
- A listener function that runs when the value changes.
- A comparison strategy (reference comparison or deep comparison).
Example:
$scope.$watch( 'user.name', function(newValue, oldValue) { console.log('Name changed:', newValue); });Internally, AngularJS stores this watcher in the scope's watcher collection.
When an event occurs, such as:
- A button click.
- An HTTP response.
- A timer callback.
- A user input event.
AngularJS starts a digest cycle.
The digest cycle performs the following steps:
- AngularJS begins checking the current scope.
- It evaluates every watcher associated with that scope.
- It compares the new value with the previously stored value.
- If a change is detected:
- The watcher listener function executes.
- The new value becomes the stored value.
- AngularJS repeats the process until no more changes are detected.
This repeated checking is called the dirty checking loop.
Simplified digest cycle example
Suppose the application has:
<p>{{username}}</p>and:
$scope.username = "John";AngularJS creates a watcher for username.
Initially:
Watcher value:"John"Later:
$scope.username = "Mike";During the digest cycle:
Previous value: JohnCurrent value: MikeAngularJS detects the difference and updates the DOM.
Why AngularJS runs multiple digest iterations
AngularJS does not assume that one watcher update is enough.
Example:
$scope.$watch('firstName', function(value) { $scope.fullName = value + " Smith";});Changing firstName modifies fullName, which may have another watcher.
AngularJS repeats the digest process until all watchers stabilize.
Internally, AngularJS uses a limit called the TTL (Time To Live) value, which is normally 10 iterations. If values continue changing after 10 iterations, AngularJS throws:
10 $digest() iterations reachedThis prevents infinite loops.
Performance impact of too many watchers
Every digest cycle checks every watcher in the application.
If an application has:
5,000 watchersand each digest cycle takes:
5,000 evaluationsthen frequent events can cause significant performance problems.
The cost increases because:
- Watchers run even if their values did not change.
- Deep watchers require additional comparison work.
- Large lists create many bindings.
- Frequent digest triggers cause repeated checks.
Common symptoms include:
- Slow page rendering.
- Laggy user interactions.
- Delayed input response.
- High CPU usage.
- Browser freezing on complex pages.
Example of watcher overload
A table displaying thousands of records:
<tr ng-repeat="employee in employees"> <td>{{employee.name}}</td> <td>{{employee.department}}</td> <td>{{employee.salary}}</td></tr>Each interpolation creates watchers.
If:
1000 employees× 3 bindings= 3000 watchersand additional application watchers exist, every digest cycle becomes expensive.
Ways to reduce watcher performance issues
Use one-time bindings
For data that does not change:
<h1>{{::title}}</h1>AngularJS removes the watcher after the value is resolved.
Use track by with ng-repeat
Instead of:
<div ng-repeat="item in items">use:
<div ng-repeat="item in items track by item.id">This helps AngularJS identify existing DOM elements efficiently.
Avoid unnecessary $watch
Avoid:
$scope.$watch( function() { return calculateLargeObject(); }, callback, true);Deep watches are expensive because AngularJS must compare object properties recursively.
Reduce unnecessary digest triggers
Avoid frequently calling:
$scope.$apply();when AngularJS already manages the digest cycle.
Real-world example
Consider an AngularJS dashboard displaying live sales information.
A developer creates:
<div ng-repeat="sale in sales"> <h3>{{sale.product}}</h3> <p>{{sale.amount}}</p> <p>{{sale.region}}</p></div>If there are 2000 sales records:
2000 records × 3 bindings = 6000 watchersEvery button click, timer update, or HTTP response triggers a digest cycle.
A better approach is to use one-time binding for data that does not change:
<div ng-repeat="sale in sales track by sale.id"> <h3>{{::sale.product}}</h3> <p>{{::sale.region}}</p> <p>{{sale.amount}}</p></div>Here:
- Product and region watchers are removed after loading.
track byimproves DOM reuse.- Only changing values continue being monitored.
This reduces digest cycle work and improves application responsiveness.
Common mistakes
- * Assuming AngularJS immediately updates the UI whenever a variable changes without a digest cycle.
- * Creating thousands of unnecessary `$watch()` expressions.
- * Using deep watches (`objectEquality = true`) for large objects without considering performance.
- * Forgetting that every interpolation like `{{value}}` creates a watcher.
- * Using large `ng-repeat` lists without `track by`.
- * Calling `$scope.$apply()` unnecessarily and triggering extra digest cycles.
- * Believing that dirty checking only checks changed variables instead of evaluating all registered watchers.
- * Ignoring memory cleanup for watchers created by custom directives.
Follow-up questions
- Why does AngularJS need multiple digest iterations instead of checking watchers only once?
- What is the difference between $watch(), $watchCollection(), and deep $watch()?
- How can you identify the number of watchers in an AngularJS application?
- Why are one-time bindings (::) useful in AngularJS?
- What causes the "10 $digest() iterations reached" error?
- How would you optimize a slow AngularJS application caused by too many watchers?