Enable Animations Explicitly For A Performance Boost In AngularJS
The other day, I came across an excellent blog post by David Chin on disabling ngAnimate animations for selected elements in AngularJS. I didn't know that this was something that could be done. And, I felt like it was important enough to write up a quick demonstration of how this can affect your page rendering performance, especially as your AngularJS applications get more complex and interactive.
Run this demo in my JavaScript Demos project on GitHub.
By default, if you include the ngAnimate module in your AngularJS application, all of the structural and class changes in your application that use the $animate service (ex, ngRepeat, ngIf, ngSwitch, ngClass) will start to examine the DOM (Document Object Model) to see if the relevant DOM elements should be animated in or out. This requires additional processing and some requestAnimationFrame() interactions. In a simple interface, this is probably not noticeable. But, as your pages get more complex, this can start to have a perceptible affect on performance, especially on pages that render lists.
To decrease the amount of work that AngularJS needs to do, you can tell it to only perform animation checks on elements that have an "enabling class". Meaning, only check for and involve animations if the target element has a particular class-name applied to it. What this does, essentially, is disable all animations until you start explicitly enabling them on a per-element basis.
This does start to blur the line a little bit between markup and presentation since the state of the markup affects the presentation. But, these two concerns are so tightly coupled already that this little extra bit of coupling shouldn't be a concern.
To see this in action, I've created a small page in which we have a single ngRepeat loop. We can swap out the list being rendered by the ngRepeat, which will trigger an animation-check for the leaving list items as well as an animation check for the entering list items. However, in this demo, I'm using the $animateProvider in the configuration / bootstrapping phase to add an "enabling class" - "animated" - for animations.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Enable Animations Explicitly For A Performance Boost In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Enable Animations Explicitly For A Performance Boost In AngularJS
</h1>
<p>
<a ng-click="showListA()">Show List A</a>
—
<a ng-click="showListB()">Show List B</a>
</p>
<ul>
<!--
We are going to be swapping the "activeList" reference to point
to either listA or listB. When we do this, ngRepeat will perform
structural changes on the DOM (Document Object Model). And, as it
does this, it will use $animate() service to transition the DOM
elements, which will do two things:
* Check to see if "leave" elements need to be animated.
* Check to see if "enter" elements need to be animated.
This behavior can be altered with the $animateProvider.
-->
<li ng-repeat="item in activeList">
{{ item }}
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.5.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [ "ngAnimate" ] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I configure the $animate service during bootstrap.
angular.module( "Demo" ).config(
function configureAnimate( $animateProvider ) {
// By default, the $animate service will check for animation styling
// on every structural change. This requires a lot of animateFrame-based
// DOM-inspection. However, we can tell $animate to only check for
// animations on elements that have a specific class name RegExp pattern
// present. In this case, we are requiring the "animated" class.
// --
// NOTE: I have personally seen a performance boost using this approach
// on some complex page. The AngularJS documentation also says that
// this can also be really beneficial for low-powered mobile devices,
// but I don't do much mobile.
$animateProvider.classNameFilter( /\banimated\b/ );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
angular.module( "Demo" ).controller(
"AppController",
function AppController( $scope ) {
// These are the lists we will be swapping in / out.
var listA = generateListItems( "A", 100 );
var listB = generateListItems( "B", 100 );
// I hold the reference to the currently selected list.
$scope.activeList = listA;
// ---
// PUBLIC METHODS.
// ---
// I select list A as the active list.
$scope.showListA = function() {
$scope.activeList = listA;
};
// I select list B as the active list.
$scope.showListB = function() {
$scope.activeList = listB;
};
// ---
// PRIVATE METHODS.
// ---
// I generate a plain-text list with the given count.
function generateListItems( suffix, count ) {
var list = [];
for ( var i = 1 ; i <= count ; i++ ) {
list.push( "List item " + i + " for " + suffix + "." );
}
return( list );
}
}
);
</script>
</body>
</html>
As you can see, during the configuration phase, I'm telling the $animate service to only execute animation checks for elements that have a class name that matches the regular expression pattern:
\banimated\b
What this means that is animations will be disabled for all elements, by default. And, only activated explicitly by the presence of the "animated" class.
To see how this affects performance, let's disable this configuration first, run the page, and swap out the list. We can see from the Chrome Dev Tools timeline how this plays out:
As you can see (and I know its hard to see in this graphic - watch the video), the very inclusion of the $animate service requires a good deal of processing and a lot of animation-frame interaction. This is true even though none of our ngRepeat items have ng-enter or ng-leave related styling.
Now, let's add the configuration-phase $animateProvider.classNameFilter() line. This time, when we run the page and swap the list, we get a Chrome Dev Tools timeline that looks like this:
With the animations disabled by default, the total scripting time drops by about an order of magnitude. This is because the $animate service isn't doing anything except checking for the existence of that "animated" class.
Of course, if I do and add the "animated" class to the ngRepeat item, the timeline reverts back to the first screenshot since AngularJS and $animate start performing the animation checks.
At InVision App, we just upgraded our AngularJS library from 1.0.8 to 1.2.22 which means that we can, for the first time, start to use ngAnimate in production. As such, this will definitely be the approach that I take with the integration - disabling all animations by default and then explicitly enabling them for certain elements. InVision App has some very complex interfaces, and I think taking this conservative, explicit approach with ngAnimate will have some decent performance benefits for our users.
Want to use code from this post? Check out the license.
Reader Comments
@All,
A quick follow-up - the difference here is more pronounced in the 1.4 rewrite in AngularJS / ngAnimate. I just did some further testing with the version of ngAnimate we are running in production - 1.2.22 - and the difference is less pronounced (~50% reduction in processing time). But, the default speed is faster, as well, in the earlier version.
That said, I'm still going to use this approach as I think it still makes sense, and will help when we next upgrade our version of AngularJS and ngAnimate.
Hi Ben,
Great article as always, thanks a lot.
So you say it still makes sense to use this approach in 1.4.x?
@Chris,
Yeah, probably. Of course, you could always wait until you have a performance issue before you do anything. You know they always say, "premature optimization is the root of all evil." But, I always think that if there are easy things you can do to have better performance, why not opt-in to them. So, I guess, bottom line is that I'll use this approach until I have a reason not to.
@Ben,
Thanks for the reply! I'll give it try and see how it goes. Always happy to find these gems here on your website I would not find anywhere else.
@Chris,
Always a pleasure :D
great tutorial as many others on this site! especially appreciate the performance inspection part on the video. thank you very much.
@Tori,
Thanks - glad you enjoyed it. The Chome dev tools are awesome. I can't even remember what I used to do back in the days of IE6. I think I used to alert() everything .... all the time :D