Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Erik Meier and Max Pappas

Looking At How scope.$evalAsync() Affects Performance In AngularJS Directives

By Ben Nadel on

In any JavaScript web application, one of the causes of user-perceived slowness can be unnecessary browser repaints. This got me thinking about AngularJS and about how directives are linked to the DOM (Document Object Model). I have seen (and have written) many directives that modify the DOM during the linking phase. This can often cause the browser to repaint, as the directive is linked, in order to provide realtime layout properties. But what if all we did was start executing DOM queries in a later part of the AngularJS $digest lifecycle? Would that have a tangible affect on performance?


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

It's hard to examine performance without a non-trivial user interface (UI); so, take this post with some skepticism. That said, I will try to demonstrate some level of complexity by using a large ngRepeat loop. In this way, any difference in performance should be measurable.

In the following demos, I have a directive - bnItem - that queries the Document Object Model for the rendered position of each element in the ngRepeat. This positional information is then used to set $scope variables. The workflow that we'll be adjusting is timing around the call to jQuery's $.fn.position() function. In the first demo, this call will be done directly in the linking function body. In the second demo, this call be moved to an $evalAsync() call.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Looking At How scope.$evalAsync() Affects Performance In AngularJS Directives
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Looking At How scope.$evalAsync() Affects Performance In AngularJS Directives
  • </h1>
  •  
  • <h2>
  • Accessing DOM Layout During Linking
  • &mdash;
  • <a ng-click="rebuild()">Rebuild</a>
  • </h2>
  •  
  • <div
  • ng-repeat="item in items"
  • bn-item
  • class="item">
  •  
  • ID: {{ item.id }}<br />
  • Coords: {{ x }} , {{ y }}<br />
  •  
  • </div>
  •  
  •  
  • <!-- 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.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the main application controller, providing data for the demo.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I hold the data being rendered in the ng-repeat.
  • $scope.items = buildItems( 1000 );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I rebuild the collection, forcing a re-rendering of the ng-repeat.
  • $scope.rebuild = function() {
  •  
  • $scope.items = buildItems( 1000 );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I return an item collection with given length.
  • function buildItems( count ) {
  •  
  • var items = new Array( count );
  • var now = ( new Date() ).getTime();
  •  
  • for ( var i = 0 ; i < count ; i++ ) {
  •  
  • items[ i ] = {
  • id: ( i + now )
  • };
  •  
  • }
  •  
  • return( items );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I demonstrate how the directive link logic can affect performance.
  • app.directive(
  • "bnItem",
  • function() {
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( $scope, element, attributes ) {
  •  
  • // Using this approach, we are accessing the condition of the DOM
  • // while the ng-repeat loop is being rendered. This will force the
  • // browser to stop and repaint after each ng-repeat node is stamped-
  • // out so that it can apply the CSS and get the positioning.
  • var position = element.position();
  •  
  • $scope.x = Math.floor( position.left );
  • $scope.y = Math.floor( position.top );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the $.fn.position() method, in the bnItem directive, queries the state of the DOM for the position of the given element. It then uses the left and top properties to set the x and y values in the $scope, respectively. When we look at how this performs using Chrome's timeline tools, we can see that the browser spends about 1.48 seconds rendering the DOM as part of the ngRepeat loop.


 
 
 

 
 Directive link performance in AnguarJS without $evalAsync(). 
 
 
 

If you look at the timeline, you can see a lot of little purple boxes. These indicate the time that the browser was rendering. What's important to see here is that there are a lot of them (many more than seen in the image) and that they are staggered. It's the staggering that kills performance, as painting is a relatively expensive process each time it happens.

Ok, now let's alter the demo just slightly to put the $.fn.position() call inside an $evalAsync() call. By doing this, we will place the DOM queries in an "async queue" that will be flushed at the start of the next $digest iteration. This should allow the ngRepeat loop in our demo to finish cloning DOM nodes before the browser has to repaint.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Looking At How scope.$evalAsync() Affects Performance In AngularJS Directives
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Looking At How scope.$evalAsync() Affects Performance In AngularJS Directives
  • </h1>
  •  
  • <h2>
  • Accessing DOM Layout Using scope.$evalSync()
  • &mdash;
  • <a ng-click="rebuild()">Rebuild</a>
  • </h2>
  •  
  • <div
  • ng-repeat="item in items"
  • bn-item
  • class="item">
  •  
  • ID: {{ item.id }}<br />
  • Coords: {{ x }} , {{ y }}<br />
  •  
  • </div>
  •  
  •  
  • <!-- 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.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the main application controller, providing data for the demo.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I hold the data being rendered in the ng-repeat.
  • $scope.items = buildItems( 1000 );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I rebuild the collection, forcing a re-rendering of the ng-repeat.
  • $scope.rebuild = function() {
  •  
  • $scope.items = buildItems( 1000 );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I return an item collection with given length.
  • function buildItems( count ) {
  •  
  • var items = new Array( count );
  • var now = ( new Date() ).getTime();
  •  
  • for ( var i = 0 ; i < count ; i++ ) {
  •  
  • items[ i ] = {
  • id: ( i + now )
  • };
  •  
  • }
  •  
  • return( items );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I demonstrate how the directive link logic can affect performance.
  • app.directive(
  • "bnItem",
  • function() {
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( $scope, element, attributes ) {
  •  
  • $scope.x = 0;
  • $scope.y = 0;
  •  
  • // By moving the DOM-query logic to an $evalAsync(), it will allow
  • // the ng-repeat loop to finish stamping out the cloned HTML nodes
  • // before the digest lifecycle goes back and starts to query for
  • // the DOM state in a later iteration. This gives the browser a
  • // chance to bulk-render the DOM.
  • $scope.$evalAsync(
  • function() {
  •  
  • var position = element.position();
  •  
  • $scope.x = Math.floor( position.left );
  • $scope.y = Math.floor( position.top );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the only thing we've changed is the body of the bnItem linking function. Now, the position-query of the element is slightly delayed until the next iteration of the $digest loop. If we examine this page, using Chrome's timeline tools, we can really see a difference:


 
 
 

 
 Directive performance in AngularJS with $evalAsync(). 
 
 
 

As you can see, this time, the browser only spent 22 milliseconds rendering. That's a huge (and very perceptible) difference. And, take a look at the timeline - notice that all of the DOM rendering was chunked at the end of the click-event. Rather than forcing the browser to render the DOM for each item in the ngRepeat loop, we allow the DOM to be fully augmented before we force the browser to do any rendering.

It seems that using $evalAsync() in an AngularJS directive can have a significant affect on performance. Of course, keep in mind that this is not a blanket statement, but rather, one applied to directives that query the DOM layout as part of their linking logic.

As a final caveat, I should say that the internal execution logic around $evalAsync() has changed between AngularJS v1.0.8 and v.1.2.x. In earlier versions, there was less of a guarantee that your asynchronous expression would execute in a timely manner. In recent versions, AngularJS has started using a timeout-based fallback to make sure your async expression executes in a timely mannor.

Also, in earlier versions of AngularJS, there were multiple "async queues" (on per scope). As such, if you're using an earlier version of AngularJS, you'll probably have better results if you inject the $rootScope into your directive and use that (as opposed to the local $scope instance).




Reader Comments

This is great! Thanks for the tip. I'll be trying out this technique very soon.

I have been using $timeout in some cases, but I think $evalAsync will be much better.

Jose

Reply to this Comment

Hi Ben, fantastic article as always. I really enjoy your writing style and the topics that you pick.

What is super nice about Angular's $evalAsync is that it gives you an 'easy' mechanism to batch any DOM actions. In your article, you talk about reads to the DOM that trigger repaints, but the same technique is effective for directives that watch multiple attributes, each of which could trigger DOM repaints.

For example, if I have a <pane size="50%" anchor="west"></pane>, both changes to the anchor and to the size target will trigger repaints. An approach that seems to work for me is to schedule updates when any DOM-related property changes. The pseudo code for that is:

$scheduleReflow = function () {
var self = this;

if (!this.$reflowScheduled) {
this.$reflowScheduled = true;

$rootScope.$evalAsync(function () {
self.repaint();
});
}
};

So each time size or anchor change, the code schedules a reflow instead of directly calling reflow. As you've clearly shown in your article, this kind of minor change can have major performance implications.

Thanks again and keep up these articles!

Reply to this Comment

@Geoff,

Really good stuff. I like the way you make sure not to double-schedule a repaint if multiple changes in the $digest lifecycle could trigger changes in your directives. Really clever.

As a concept, I think AngularJS does something similar in the $evalAsync() method itself - in AngularJS 1.2, the $evalAsync() will start a $timeout() in order to make sure things don't sit in the $$asyncQueue forever. But, it will only set up the timeout if one doesn't already exist, in order to ensure that multiple timeouts don't end up firing.

Good stuff!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.