Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Sam Effa
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Sam Effa

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

By
Published in Comments (8)

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

Want to use code from this post? Check out the license.

Reader Comments

2 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

1 Comments

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!

15,848 Comments

@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!

2 Comments

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?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel