Using ngRepeat With ngInclude Hurts Performance In AngularJS
Yesterday, I took a quick look at the performance impact of using directive templates in AngularJS. The impact was small and definitely worthwhile considering the benefits of creating reusable code. Today, however, I wanted to look at another approach to reusing code that does have a significant performance impact: using ngRepeat with ngInclude in AngularJS.
Run this demo in my JavaScript Demos project on GitHub.
For this experiment, as with yesterday's, we're going to render two different lists that use the same content. In order to keep the code DRY (Do not Repeat Yourself), we're going to define the ngRepeat content as template that gets included using the ngInclude directive. I'm not going to bother showing the "control" experiment - the code from yesterday (you can watch the video); so, let's jump right into the ngRepeat-ngInclude demo:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Using ngRepeat With ngInclude Hurts Performance In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Using ngRepeat With ngInclude Hurts Performance In AngularJS
</h1>
<h2>
Using ngRepeat With ngInclude
</h2>
<p>
<a ng-click="toggleLists()">Toggle Lists</a>
</p>
<div
ng-if="isShowingLists"
ng-include=" 'list.htm' ">
<!-- Content pulled-in as template to simulate real-world architecture. -->
</div>
<!--
I am the template used to render the main page.
--
NOTE: Both ngRepeat directives identify their item as "person" because the
ngInclude'd template does not have the ability to differentiate between different
contexts (like an attribute-based binding might).
-->
<script id="list.htm" type="text/ng-template">
<div class="friends">
<h2>
Friends
</h2>
<ul>
<li
ng-repeat="person in friends track by person.id"
ng-include=" 'person.htm' ">
<!-- Content provided by ngInclude. -->
</li>
</ul>
</div>
<div class="enemies">
<h2>
Enemies
</h2>
<ul>
<li
ng-repeat="person in enemies track by person.id"
ng-include=" 'person.htm' ">
<!-- Content provided by ngInclude. -->
</li>
</ul>
</div>
</script>
<!-- I am the template included by ngInclude. -->
<script id="person.htm" type="text/ng-template">
{{ person.id }} — {{ person.name }}
</script>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.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 ) {
// I hold the lists being rendered.
$scope.friends = buildList( "Sarah", 1000 );
$scope.enemies = buildList( "Shane", 1000 );
// I determine if the lists should be shown.
$scope.isShowingLists = false;
// ---
// PUBLIC METHODS.
// ---
// I toggle the rendering of the lists (physically removing them from
// the page if they should not be there).
$scope.toggleLists = function() {
$scope.isShowingLists = ! $scope.isShowingLists;
};
// ---
// PRIVATE METHODS.
// ---
// I build a list of people using the given size.
function buildList( name, count ) {
var people = [];
for ( var i = 1 ; i <= count ; i++ ) {
people.push({
id: i,
name: name
});
}
return( people );
}
}
);
</script>
</body>
</html>
As you can see, each ngRepeat template also uses the ngInclude directive to reference a common template. Notice that the ngRepeat iteration item is "person" in both cases. We have to do this because the included template has no way to differentiate the calling context (ie, friend vs. enemy).
This page works just as you would expect; however, it takes over a second to render (where as the control takes about 100ms). And, when we look at the Chrome dev-tools timeline, we can see why:
The browser spends over a second just parsing HTML. The reason for this is that the ngInclude directive is recompiling the content of the included template in every single linking phase of the ngRepeat. This means that if there are 100 ngRepeat clones, the ngInclude template gets compiled 100 times. This is a striking difference when compared to the use of a directive template which only compiles the template once.
NOTE: After digging through the AngularJS source code, I believe the directive only compiles the template once by queuing up the linking functions. Then, once the template is available, AngularJS clones and links the previously-compiled node using the queued transclusion functions. Or, as best I can tell - this portion of the AngularJS code is particularly cryptic.
I think the lesson learned here is that if you are using ngInclude to reuse code (such as inside an ngRepeat), it's probably better off inside a "component" directive. This will create a more responsive experience for your users. Of course, if you're using ngInclude to render sections of a page in a non-reusable manner, using ngInclude should be totally fine - ngInclude is awesome, I'm not trying to give it a bad name.
Want to use code from this post? Check out the license.
Reader Comments
The more I get into directives, the more I realize the power they hold.
@Phil,
They are pretty cool. I'm still learning about how to best incorporate them; and how to think about what goes in the directive vs. what goes in the controller. But, definitely growing to like them more.
I've been looking at React.js recently and the viritual DOM really helps for actions like this. I may have heard Angular 2 is addressing this too. More than a few performance tests comparing the two out there.
@Brett,
From what I have heard, ReactJS can be faster than AngularJS when Angular does a lot of DOM creation and destruction. However, if you can get AngularJS to limit the amount of DOM manipulation it does, I've heard that the two libraries are mostly on-par with performance.
This is why I thought the "track by" feature of ngRepeat was the most exciting update of AngularJS 1.2:
www.bennadel.com/blog/2556-using-track-by-with-ngrepeat-in-angularjs-1-2.htm
... it allows the ngRepeat data to be refreshes *without* creating new elements (unless of course there are new items in the collection).
That said, this is all from things I've heard in passing. I'm curious to check out the ReactJS framework in general.
Hi Ben
I think we can do better on caching in ngInclude. Here is a proof of concept :
https://github.com/angular/angular.js/pull/10432
and a Plunker:
http://plnkr.co/edit/Pf9DVKxNEAZQavcGm7Bu?p=preview
It is no longer hitting renderHTML all the time but there is still a considerable lag - probably due to DOM manipulation and GC.
@Pete,
Very cool. I tried to look through the part of the AngularJS source code that deals with how directives handle the templateUrl. I don't know how you guys follow some of that code - there is *so much going on*! I guess once you know build the mental model and know where to look, it gets easier.
I keep going back to how the ngInclude and the directive templateUrl differ; because, at first glance, it feels like they are doing the same thing - including a template and replacing content.
But, once you start to noodle on it a bit more, you realize that the ngInclude SRC can be dynamic, where as the directive template cannot. Meaning, we always know exactly what the directive template is going to be, even in the context of an ngRepeat; but, the ngInclude src can actually be different for every ngRepeat clone (if the path is derived from the $index, for example). As such, I assume you can't make the same assumptions with an ngInclude that you can with a directive.
@All,
After I posted about this, Igor Minar challenged me with the idea that all ngInclude directives could be replaced with "component directives." I attempted my first exploration in this vein:
www.bennadel.com/blog/2740-replacing-nginclude-with-component-directives-in-angularjs.htm
It's actually pretty cool because it combines all three ingredients - Controller, View, and Link function. And, since it's a directive tempatelUrl, it has added optimizations, related back to what I was just discussing with Pete (above).
If It works wrong the cause is a wrong API program (Google) because the problem only could be occurred with the get of the template but it is cached. Of course this is not a problem. If the problem is the $compile the process should be the same that you wouldn't have the ng-include. I don't know where can be the neck bottle.
@benndale I have seen the google recomendation to use www.bennadel.com/blog/2740-replacing-nginclude-with-component-directives-in-angularjs.htm but how can I do it with ng-repeat? I need to do a directive inside ng-repeat with the content that I want?
Perhaps I missed this piece of information, but why not cache the output of $compile?
Might give serious boost to anyone using ng-include massively enough.
It can even be a flag on attribute..
Any reason it was not implemented that way?
@Guy,
I don't think you missed anything :) I am not sure why they wouldn't just cache the compile-result. That said, ngInclude isn't even a feature of Angular 2. For the most part, all the frameworks are moving into a "component" oriented structure. So, rather than ever ngInclude'ing a file, you're matching an element selector that renders a component with its own template.