Using $scope.$digest() As A Performance Optimization In AngularJS

The other day, I was listening to Brian Genisio, on the Front-End Developer Cast, when I heard him draw a distinction between the $apply() method and the $digest() method in AngularJS. The $apply() method will trigger watchers on the entire $scope chain whereas the $digest() method will only trigger watchers on the current $scope and its children. Definitely a specialized case - but, this struck me as an opportunity for some micro-optimizations in my AngularJS applications.

Since AngularJS is driven by dirty-data checks, it has to run through all of its watchers whenever it thinks the view-model has changed. Then, it has to run through all of its watchers again, to make sure that the last iteration of watchers didn't change the view-model. Then it has to do this again to - ... you get the point. It's an awesome approach; but, when you have a lot of $watch bindings, it can require a lot of [sometimes unnecessary] processing.

The $digest() method can offer an optimization in the dirty-data lifecycle in situations where you know - for a fact - that local changes will not have global implications. In such situations, the $digest() method can limit the scope of processing (no pun intended) to the local branch of the application's $scope chain.

To demonstrate this, I've created a small AngularJS application that uses two different mouse-interaction directives. On one element, it uses the native ngMouseEnter and ngMouseLeave directives; on another element, it uses a custom bnDigest directive which handles the mouse-interaction logic more manually.

To illustrate the point, the bnDigest directive has to create a child scope. Otherwise, calling the $digest() method would be tantamount to calling the $apply() method and you wouldn't be able to see the distinction.

<!doctype html>

<html ng-app="Demo">

<head>

<meta charset="utf-8" />

<title>

Using $scope.$digest() As A Performance Optimization In AngularJS

</title>

<link rel="stylesheet" type="text/css" href="./demo.css"></link>

</head>

<body ng-controller="AppController">

<h1>

Using $scope.$digest() As A Performance Optimization In AngularJS

</h1>

<!--

Notice that both this P-tag and the next one both use ng-class to see CSS

classes based on the Model; however, the second P uses a child scope (created

// Setting up a watcher to mimic the high number of bindings that most

// applications will have. I will be called on every digest.

$scope.$watch(

function() {

console.log( "Top-level digest 1." );

}

);

// Setting up a watcher to mimic the high number of bindings that most

// applications will have. I will be called on every digest.

$scope.$watch(

function() {

console.log( "Top-level digest 2." );

}

);

// Setting up a watcher to mimic the high number of bindings that most

// applications will have. I will be called on every digest.

$scope.$watch(

function() {

console.log( "Top-level digest 3." );

}

);

// I determine if the target element is "hot" (for display purposes).

$scope.isHot = false;

// ---

// PUBLIC METHODS.

// ---

// I set the new isHot property.

$scope.setIsHot = function( newIsHot ) {

$scope.isHot = newIsHot;

};

}

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// I implement some mouse-interaction behavior without triggering digests at a

// higher level in the $scope chain.

app.directive(

"bnDigest",

function() {

// I bind the JavaScript events to the scope.

function link( $scope, element, attributes ) {

// I determine if the target element is hot.

$scope.localIsHot = false;

// I activate the element on mouse-enter.

element.mouseenter(

function() {

$scope.localIsHot = true;

// NOTE: By calling the $digest() instead of the more typical

// $apply() method, we will only trigger watchers on the local

// scope (and its children). We will NOT trigger any watchers

// on the parent scope.

$scope.$digest();

}

);

// I deactivate the element on mouse-leave.

element.mouseleave(

function() {

$scope.localIsHot = false;

// NOTE: By calling the $digest() instead of the more typical

// $apply() method, we will only trigger watchers on the local

// scope (and its children). We will NOT trigger any watchers

// on the parent scope.

$scope.$digest();

}

);

}

// NOTE: By setting scope to TRUE, the directive creates a new child scope

// that separates it from the parent scope (creating a isolated part of

// the scope chain).

return({

link: link,

restrict: "A",

scope: true

});

}

);

</script>

</body>

</html>

As you can see, the bnDigest updates the "localIsHot" view-model value and then calls the $digest() method. This works because the bnDigest directive has intimate knowledge of how the localIsHot view-model is being used; and, it knows that none of the higher-up $scope objects need to know about it. As such, it can safely trigger a local-only digest without leaving the view in a partially-rendered state.

As you're probably thinking, this requires the directive to know a lot about how it's being used. That's right. That's why this is not a general optimization but, rather, an optimization that can only be used when a directive is acting more like a "view helper" and less like a general event-binding. That said, in some types of situations, this can be an easy-to-implement performance optimization.

Great find! I have lots of drag & drop on my web app which has the potential to trigger many 'applies' per second, and even after optimizing heavily it could still feel a bit slow at times. There's a lot of stuff going on on the page so being able to digest only a certain scope during drag & drop eliminated any perceived lag.

I am loving your articles, you go much deeper into the topics that really matter when truly understanding a lot of the "magic" behind angular. I agree with you that this is a great trick for *spot* optimizations..however I have lately seen it crop up in a couple open source libraries and that really feel far too limiting for their use case, so I'd like to reiterate that you should only optimize when it really matters. (especially when limiting the scope of the digest directly limits the access of directives/controllers up the chain)

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments

Live in the Now

Oops!

Name:

Email:

( I keep this private )

Website:

Comment:

Subscribe to comments.

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please
do not post unrelated questions or
large chunks of code. And, above all, please be nice to each other - we're trying to
have a good conversation here.