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.
Run this demo in my JavaScript Demos project on GitHub.
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
by the bn-digest directive).
-->
<p
ng-mouseenter="setIsHot( true );"
ng-mouseleave="setIsHot( false );"
class="apply"
ng-class="{ hot: isHot }">
$apply()
</p>
<p
bn-digest
class="digest"
ng-class="{ hot: localIsHot }">
$digest()
</p>
<p class="logging">
<em>NOTE: Logging output to console.</em>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// 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.
Want to use code from this post? Check out the license.
Reader Comments
Oh that's a nice catch, I'll definitively use it next time I can :)
@Olivier,
Thanks my man!
Good one. Definitively recommended $digest in place of $apply.
@Kuldeep,
It's a small optimization, and it doesn't make sense all the time. But, in some situations, definitely a boost.
Agreed Ben. Definitely Boost in some cases where we can use.
@All,
As a follow-up to this post, I created a concrete case-study of when the $digest() approach really makes sense:
www.bennadel.com/blog/2596-Case-Study-Using-scope-digest-As-A-Performance-Optimization-In-AngularJS.htm
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)
Here is the example that seems to show that also $scope.$digest() updates
outside scopes. Or did I miss anything?
http://jsbin.com/beniha/1/edit
And now just pass the $digest of your directive/controller to a service/provider/facotry/whatever, and you have the capacity to make a local update of your view when you have an aync model change.
And to be perfect, change all ng-click, ng-mouseover ... ng-IFireAScopeApply to digest-click, digest-mouseover ...
Or use broadcast service => directive/controller, each can make the $digest if it's needed, this solution has the advantage to prevent coupling.
Inspired by you and angular source, event without $apply :
https://github.com/vincentEuloge/angularTips/blob/master/eventWithoutApply.js
Nice article sir . I also tried to write on this may be it will help someone
http://javacourseblog.blogspot.in/2014/06/angularjs-apply-digest-in-detail-with.html