The $digest
cycle is the critical entity for keeping our Angular applications fast: the faster the cycle, the faster the two-way data binding. JavaScript has a single thread of execution, which means if our $digest
cycle is packed full of data to be dirty-checked, the user is going to see lag in the UI whilst (for instance) typing inside an ``.
$digest
cycles run from internal Angular events (yes and $scope.$apply()
) built into their Directives, such as ng-click
, ng-change
and so on. When something triggers these internal events, such as a keypress
that triggers an <input>
with ng-model
bound to it, Angular will run the $digest
loop to see if anything has changed. If something has changed, Angular will update the bound JavaScript Model. If something in the Model changes, Angular will run the $digest
again to update the View. Basics of “dirty-checking” - simple.
Table of contents
Problem
The problem with dirty-checking is that larger $digest
loops will take longer to complete, which means the user could see some lag whilst using the application. Understanding the performance impacts upfront can help you build better applications using the right APIs.
For this article, I’ve written a tiny Directive that logs our $digest
cycle counts, so we can actually see the impact that simple UI interactions may have on our applications.
Let’s take a simple <input>
for this example with ng-model
bound to it:
<input
type="text"
ng-model="test">
The live output:
Type away, you’ll see the $digest
count rockets into double and triple figures. Now, with data heavy applications this is going to cause some severe lag for the end user.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Solution
Using ngModelOptions
, a Directive introduced in Angular 1.3, we can add a debounce to <input>
triggers so we have complete control over how and when $digest
cycles occur.
Let’s add the ng-model-options
attribute and tell Angular to update our Model on the blur
event only using the updateOn
property:
<input
type="text"
ng-model="test"
ng-model-options="{
'updateOn': 'blur'
}">
Type a bunch of characters in, and then click/tab out the <input>
to see the $digest
count increase.
This is a massive performance improvement as we’re not continuously running $digest
. We’re not limited to a single event, so let’s add 'default'
as an option:
<input
type="text"
ng-model="test"
ng-model-options="{
'updateOn': 'default blur'
}">
With the default
value also added, we’re back to square one where $digest
will run each time the user types. Time to get smart! Let’s tell Angular to periodically update the Model, so that other bindings that may rely on the Model being persisted will be updated too. We can introduce the debounce
property inside the ng-model-options
Object to tell Angular exactly when to update the Model after specific events occur.
<input
type="text"
ng-model="test"
ng-model-options="{
'updateOn': 'default blur',
'debounce': {
'default': 250,
'blur': 0
}
}">
The above illustrates that default
will be updated 250ms
after the event stops, and blur
will update immediately as the user leaves the input (if this is the desired behaviour we want).
Start typing again, then stop and note the $digest
count is severely lower than the initial demonstration. You can then click/tab out the <input>
to call another $digest
immediately.
$digest tracking Directive
If anyone is interested in using the code from the Directive for testing purposes feel free:
/**
* trackDigest.js
* Simple counter for counting when $digests occur
* @author Todd Motto
*/
function trackDigests($rootScope) {
function link($scope, $element, $attrs) {
var count = 0;
function countDigests() {
count++;
$element[0].innerHTML = '$digests: ' + count;
}
$rootScope.$watch(countDigests);
}
return {
restrict: 'EA',
link: link
};
}
angular
.module('app')
.directive('trackDigests', trackDigests);
Doesn’t really need annotating, the key ingredient here is $rootScope.$watch
with just a function inside, which will run every $digest
. Inside the callback I’m simply setting the innerHTML
of the $element[0]
reference to the updated count after incrementing with count++
.