Write AngularJS like a pro. Angularjs Icon

Follow the ultimate AngularJS roadmap.

Superfast Angular use ngModelOptions to limit $digest cycles

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.

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.

Angular Directives In-Depth eBook Cover

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.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon 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++.

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover