Performance Optimization for Long Lists in AngularJS

Original author: Sebastian Fröstl
  • Transfer
  • Tutorial
AnglarJS is great! But when working with large lists containing complex data structures, it can start working very slowly! We encountered this problem when porting our admin panel to AngularJS. It should have worked without delay when displaying about 500 lines. But the first display took up to 7 seconds. Awful
We found two bottlenecks in our implementation. One was related to the directive ng-repeat, and the other to apply filters.
This article talks about the results of our experiments with various approaches to solve or mitigate a performance problem. This will give you ideas and tips on where you can put your strength and which approaches should not be used.

Why does the ng-repeat directive work slowly with large lists?

The directive ng-repeatbegins to work slowly if there is a two-way binding to lists with more than 2500 elements. You can read more about this in the Misko Hevery post . This is because AngularJS tracks changes in a dirty check way. Each change tracking will take some time, which for large lists with a complex data structure results in slowing down your application.

Prerequisites Used for Performance Analysis


Tracking the working hours of the directive:
To measure the time the list was displayed, we wrote a simple directive that measures the duration of work ng-repeatusing its property $last. The base date is stored in our service TimeTracker, so the result does not depend on downloading data from the server.
// директива для ведения журналов времени отображения
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 отобразился за: " + (end - ref) + " ms");
         });
       }
    };
  }
]);

Usage in HTML:

Features of chronology tracking using development tools in Chrome
On the chronology tab (timeline) of the Chrome developer tools, you can see events, browser frames per second (frames), and memory allocation. The memory tool is useful for detecting memory leaks and for determining the amount of memory your application needs. Page flickering becomes a problem when the frame refresh rate is less than 30 frames per second. The frames tool displays page performance information. In addition, it displays how much time the CPU consumes javascript.

Basic settings limiting the size of the list


The best way to mitigate this problem is to limit the size of the displayed list. This can be done by pagination or by endless scrolling.

Pagination

Our pagination method is based on a combination of the AngularJS filter limitTo(since version 1.1.4) and our filter startFrom. This approach reduces display time by limiting the size of the displayed list. This is the most effective way to reduce display time.
// Разбиение на страницы в контроллере
$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);
};
// наш фильтр startFrom
angular.module('app').filter('startFrom', function() {
    return function(input, start) {         
        return input.slice(start);
};

Usage in HTML.

If you do not want or cannot use pagination , but you are still worried about the problem of slow filters, do not be too lazy to look at step 5, and use ng-hideto hide unnecessary list items.

Endless scrolling

In our project, we did not consider the option with infinite scrolling. If you want to explore this feature more deeply, you can visit the endless scroll project for AngularJS .

Optimization Guidelines


1. Display a list without data bindings

This is the most obvious solution, since it is data binding that causes performance problems. Getting rid of data binding is great if you just want to display the list once, and there is no need to update or change the data. Unfortunately, in this case, control on the data is lost, which did not suit us. To whom it is interesting, in addition look at the bindonce project .

2. Do not use the built-in method call to get data

Do not use the method to get the filtered collection to get the filtered list directly in the controller. ng-repeatcomputes all expressions on each $ digest cycle , i.e. this is done very often. In our example, it filteredItems()returns a filtered collection. If it runs slowly, it will quickly slow down the entire application.

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

    The meaning of this useful template is to separate the display list from the data list. This allows you to pre-apply multiple filters and apply collection caching to the view. The following example shows a very simplified implementation. The variable filteredListsrepresents the collection cache, and the method applyFilteris responsible for matching.
    /* Контроллер */
    // Базовый список 
    var items = [{name:"John", active:true }, {name:"Adam"}, {name:"Chris"}, {name:"Heather"}]; 
    // инициализация отображаемого списка
    $scope.displayedItems = items;
    // Кэш фильтров
    var filteredLists['active'] = $filter('filter)(items, {"active" : true});
    // Применение фильтра
    $scope.applyFilter = function(type) {
        if (filteredLists.hasOwnProperty(type){ // Проверка наличия фильтра в кэше
            $scope.displayedItems = filteredLists[type];
        } else { 
            /* Если фильтр не закэширован */
        }
    }
    // Сброс фильтров
    $scope.resetFilter = function() {
        $scope.displayedItems = items;
    }
    

    In view:
    • {{item.name}}


    4. Use ng-if instead of ng-show to complement templates

    If you use additional directives or templates to display additional information on the list item, if you click on it, use ng-if (since version 1.1.5). ng-ifprohibits display (as opposed to ng-show). In this case, additional elements are added, and bindings are resolved exactly when they are needed.
  • {{ item.title }}


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

    In our opinion, using the built-in AngularJS directive ng-mouseentercaused the screen to flicker. The frame rate in the browser was in most cases below 30 frames per second. Using pure jQuery to create animation effects and hover effects will help solve this problem. Remember to only wrap mouse events with jQuery.live () to receive notifications from elements added to the DOM later.

    6. Setting properties for filtering. Hide excluded items with ng-show

    With long lists, filters also work more slowly, since each filter creates its own subset of the original list. In many cases, when the initial data does not change, the result of applying the filter remains the same. To use this, you can pre-filter the list of data, and apply the filter result at the time when it will be needed, saving on processing time.
    When applying filters with a directive ng-repeat, each filter returns a subset of the original collection. AngularJS also removes filter-excluded elements from the DOM, and raises an event $destroythat removes them from $scope. When the input collection changes, a subset of the elements passed through the filter also changes, which again causes them to be redrawn or destroyed.
    In most cases, this behavior is normal, but in the case when the user often uses filtering or the list is very large, continuous linking and destruction of elements greatly affects performance. To speed up filtering, you can use the ng-showand directives ng-hide. Calculate the filters in the controller and add a property for each element. Use ng-showwith the value of this property. As a result of this, the directive ng-hidewill simply add a specific class, instead of removing elements from a subset of the original collection, $scopeand the DOM.
    • The first way to invoke ng-showthis is to use expression syntax. An expression is ng-showevaluated using the built-in filter syntax.
      See also the following plunkr example .
    • {{item.name}}

    • Another way is to transfer a certain value through the attribute in ng-show, and further calculations, based on the transferred value, in a separate subcontroller. This method is somewhat more complicated, but its application is cleaner, which Ben Nadel substantiates in his blog article .

    7. Setting filtering hints: sending input data

    Another way to solve the problem of repeated filtering, in addition to the methods described in clause 6, is to send user input. For example, if the user enters a search string, the filter simply needs to be activated after the user has finished entering.
    A good example of using this approach is the following service . Use it in your view and controller as follows:
    /* Контроллер*/
    // Отслеживание ввода и пересылка в систему фильтрации каждые 350 мс.
    $scope.$watch('queryInput', function(newValue, oldValue) {
        if (newValue === oldValue) { return; }
        $debounce(applyQuery, 350);
    });
    var applyQuery = function() { 
        $scope.filter.query = $scope.query;
    };
    

    /* Представление*/
    
  • {{ item.title }}

  • For further reading


    1. Project organization for huge applications
    2. Misko Hevery's StackOverflow answer to a question regarding data binding performance in Angular
    3. Short article on various ways to improve ng-repeat performance
    4. Download big data on demand
    5. Good scope article
    6. AngularJS project for dynamic templates
    7. Display without data binding

    Also popular now: