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

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?

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

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.

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

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:

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).

Depending on your version of AngularJS, there are some important points discussed in the Comments of that post. Specifically, if you're on 1.0.8 (or earlier), there are some timing caveats to be aware of with $evalAsync().

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.

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.

After reading your article, it feels like $evalAsync is preferred to $apply (and I was thinking of replacing my $apply with $evalAsync):- it allows batching of things- it prevents "Error: $digest already in progress" (useful is a method may do some async call or return something from the cache synchronously)

But in the doc it's written:Note: if this function is called outside of a $digest cycle, a new $digest cycle will be scheduled. However, it is encouraged to always call code that changes the model from within an $apply call. That includes code evaluated via $evalAsync.

Any comment on that?

Reply to this Comment

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.