AngularJS Performance Tuning for Long Lists

AnglarJS is great! But when dealing with large lists containing complex data structure, things can get very slow! We ran into that problem when migrating our core admin screens to AngularJS. The screens were supposed to work smoothly when displaying some 500 rows. But the first approach took up to 7 seconds to rende. Terrible!

We discovered two main performance issues for our implementation. One is related to the ng-repeat directive, the other was related to the filtering.

The following article summarizes our experiences with different approaches to solve or mitigate the performance problem. It will give you ideas and hints, what you can try out yourself and what is maybe not be worth a try.

Hint: This post is from 2013. We’ve migrated to React in the meantime. Are you keen to work on React in Berlin? Check our our careers page!

Why is ng-repeat in AngularJS slow with large lists?

The ng-repeat directive of AngularJS is getting slow above 2500 two-way data bindings. You can read more about this in a post by Misko Hevery (See further reading no 2). This is due to AngularJS watching for changes by “dirty checking”. Every watch consumes time, so large lists with complex data structure will slow down your application.

Useful prerequisites for analyzing performance

Time logging directive:

To measure the time a list rendering takes we wrote a simple directive, which logs the time by using the ng-repeat property “$last”. The reference date is stored in our custom TimeTracker-service, so the logging is independent of data loading from the server.

   
// Post repeat directive for logging the rendering time
angular.module('siApp.services').directive('postRepeatDirective', 
  ['$timeout', '$log',  'TimeTracker', 
  function($timeout, $log, TimeTracker) {
    return function(scope, element, attrs) {
      if (scope.$last){
         $timeout(function(){
             var timeFinishedLoadingList = TimeTracker.reviewListLoaded();
             var ref = new Date(timeFinishedLoadingList);
             var end = new Date();
             $log.debug("## DOM rendering list took: " + (end - ref) + " ms");
         });
       }
    };
  }
]);

// Use in HTML:
<tr ng-repeat="item in items" post-repeat-directive>…</tr>
Timeline feature of Chrome developer tools

In the timeline tab of Chromes developer tools, you can see events, the browsers frames per second and memory allocation. The memory tool is useful to detect memory leaks and to see how much memory your page needs. Page flickering is mostly a problem when the frame rate is below 30 frames per seconds. The frames tool provides insight into the rendering performance. Additionally it displays how much CPU time a JavaScript task consumes.

Basic tuning by limiting size of list

The best way to mitigate the problem is by limiting the size of the displayed list. You can do that by pagination, or by infinite scrolling.

Pagination

Our way to paginate is to combine the AngularJS ‘limitTo’ filter (since version 1.1.4) and a custom ‘startFrom’ filter. This setting allows us to reduce rendering time by limiting the size of the displayed list. It is the most effective way to reduce rendering time.

    
// Pagination in controller
$scope.currentPage = 0; 
$scope.pageSize = 75;
$scope.setCurrentPage = function(currentPage) {
    $scope.currentPage = currentPage;
}

$scope.getNumberAsArray = function (num) {
    return new Array(num);
};

$scope.numberOfPages = function() {
    return Math.ceil($scope.displayedItemsList.length/ $scope.pageSize);
};

// Start from filter
angular.module('app').filter('startFrom', function() {
    return function(input, start) {         
        return input.slice(start);
};

// Use in HTML
// Pagination buttons
<button ng-repeat="i in getNumberAsArray(numberOfPages()) track by $index" ng-click="setCurrentPage($index)">{{$index + 1}}</button

// Displayed list
<tr ng-repeat="item in displayedItemsList | startFrom: currentPage * pageSize  | limitTo:pageSize" /tr>

If you can’t or don’t want to use pagination, but you have problems with slow filtering, be sure to check out step 5 and use ng-show to hide excluded elements.

Infinite scrolling

For our use case infinite scrolling was not an option. If you want to dive deeper into that, the following link leads to an AngularJS infinite scrolling project:
http://binarymuse.github.io/ngInfiniteScroll/

Tuning guidelines

1. Render the list without data binding

This is the most obvious solution, since data-binding is most likely the source of the performance woes.  Ditching the data binding is absolutely fine if you just want to display the list once, and there is no need to deal with updates or changes. Unfortunately you lose control of the data, so this approach was no option for us. Further reading: https://github.com/Pasvaz/bindonce

2. Do not use a inline method call for calculating the data

In order to filter the list directly in the controller, do not use a method for getting the filtered collection. Ng-repeat evaluates the expression on every [$digest(http://docs.angularjs.org/api/ng.$rootScope.Scope#$digest)], which is done very often. In our example the “filteredItems()” returns the filtered collection. If this evaluation is slow, it will quickly slow down the whole application.

    
<li ng-repeat="item in filteredItems()"> // Bad idea, since very often evaluated.
<li ng-repeat="item in items"> // Way to go! 

3. Use two lists (one for the view to display, one as data source)

A useful pattern is to separate the displayed list and the data list. This allows you to preprocess some filters and apply the cached collections to the view. The following example shows a very basic implementation. The filteredLists variable is holding the cached collections, the applyFilter method handles the mapping.


/* Controller */
// Basic list 
var items = [{name:"John", active:true }, {name:"Adam"}, {name:"Chris"}, {name:"Heather"}]; 

// Init displayedList
$scope.displayedItems = items;

// Filter Cache
var filteredLists['active'] = $filter('filter)(items, {"active" : true});

// Apply the filter
$scope.applyFilter = function(type) {
    if (filteredLists.hasOwnProperty(type){ // Check if filter is cached
        $scope.displayedItems = filteredLists[type];
    } else { 
        /* Non cached filtering */
    }
}

// Reset filter
$scope.resetFilter = function() {
    $scope.displayedItems = items;
}

/* View */
<button ng-click="applyFilter('active')">Select active</button>
<ul><li ng-repeat="item in displayedItems">{{item.name}}<li></ul>

4. Use ng-if instead of ng-show for additional templates

If you use directives or templates to render additional information, for instance to display details of a list item on click, be sure to use ng-if(since v. 1.1.5). Ng-if  prohibits rendering (in contrast to ng-show). Therefore the additional DOM elements and data-bindings are evaluated on demand.

    
<li ng-repeat="item in items">
    <p> {{ item.title }} </p>
    <button ng-click="item.showDetails = !item.showDetails">Show details</buttons>
{{item.details}}

</li>

5. Do not use AngularJS directives ng-mouseenter, ng-mouseleave, etc.

Using the built-in directives like ng-mouseenter AngularJS caused our view to flicker. The browser frame rate was mostly below 30 frames per second. Using pure jQuery to create animations and hover-effects solved this problem. Be sure to wrap the mouse events in jQuery’s .live() function – to be aware of later DOM changes.

6. Tuning hint for filtering: Hide elements with ng-show that are excluded

With long lists, the use of filters will also work slower, due to each filter createing a sub collection of the original list. In many cases, when the data does not change, filter results stay the same. Therefor a pre-filtering of the data list and accordingly applying it to the view saves processing time.
By applying filters in the ng-repeat directive, each filter returns a subset of the original collection. AngularJS is removing the excluded elements from the DOM and, by calling $destroy, also removes them from the $scope. When the filter input changes, the subset changes and elements have to be relinked or $destroyed again.
In most cases this is absolutely fine, but in case the user filters often, or the list is very large, this continuous linking and destroying impacts performance. To speed up the filtering, you can use ng-show and ng-hide directives. Calculate the filters in the controller and add a property to each item. Trigger ng-show depending on that property. As a result, this will only add the class ng-hide to the elements instead of removing them from sub-list, $scope and DOM.

  • One way to trigger ng-show is to use the expression syntax. The ng-show value is evaluated by using the build in filter syntax.
    See also the following plunkr example
     
    <input ng-model="query"></input>
    <li ng-repeat="item in items" ng-show="([item.name] | filter:query).length">{{item.name}}</li>
    
  • Another way to do that is to use pass in an attribute for the ng-show and do the computation for that value in a separate sub-controller. This way is a bit more complex, yet cleaner way like Ben Nadel suggests in his blog post

7. Tuning hint for filtering: Debounce input

Another way to solve the continuous filtering described at point 6, is to debounce user input. For instance, if a user types a search string, the filter only needs to be activated after the user stops typing. A good solution is to use this debounce service http://jsfiddle.net/Warspawn/6K7Kd/. Apply it in your view and controller as follows:

    
/* Controller */
// Watch the queryInput and debounce the filtering by 350 ms.
$scope.$watch('queryInput', function(newValue, oldValue) {
    if (newValue === oldValue) { return; }
    $debounce(applyQuery, 350);
});
var applyQuery = function() { 
    $scope.filter.query = $scope.query;
};

/* View */
<input ng-model="queryInput"/>
<li ng-repeat= item in items | filter:filter.query>{{ item.title }} </li>

Further reading

  • 1. Project organization with huge apps: http://briantford.com/blog/huuuuuge-angular-apps.html
  • 2. Stackoverflow answer of Misko Hevery concerning angular data-binding performance: http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933
  • 3. Short article with different approaches increase ng-repeat performance: http://www.williambrownstreet.net/blog/?p=437
  • 4. Loading more data on request: http://stackoverflow.com/questions/17348058/how-to-improve-performance-of-ngrepeat-over-a-huge-dataset-angular-js
  • 5. Good article on using the scope: http://thenittygritty.co/angularjs-pitfalls-using-scopes
  • 6. AngularJS project for dynamic templates http://onehungrymind.com/angularjs-dynamic-templates
  • Rendering without data-binding: https://github.com/Pasvaz/bindonce

Got feedback or ideas?

We’d love to hear your thoughts! It took us some time to figure out these hints. We were not able to find any comprehensive guidelines on this matter.  Leave a comment below if you have further suggestions, or happen to know other performance-related guidelines. We’d love to mention them, or to link to your post if you have one.