⬆️ ⬇️

Optimize long list performance in AngularJS

AnglarJS is great! But when working with large lists containing a complex data structure, it can start working very slowly! We encountered this problem when porting our administrative panel to AngularJS. It should work 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 associated with the ng-repeat directive, and the other with the use of filters.

This article tells about the results of our experiments with different approaches to solve, or mitigate, the problem with performance. This will give you ideas and advice, where you can put your strength, and which approaches you should not use.



Why is the ng-repeat directive slow with large lists?


The ng-repeat directive starts to work slowly if the two-way binding to lists with more than 2500 elements is implemented. You can read more about this in the post Misko Hevery . This is due to the fact that in AngularJS, changes are tracked by the “dirty check” method. Each change tracking will take some time, which for large lists with a complex data structure results in slowing down your application.



Used assumptions for performance analysis



Tracking time directive work:

To measure the display time of the list, we wrote a simple directive that measures the duration of ng-repeat using its $last property. The base date is stored in our TimeTracker service, 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"); }); } }; } ]); 


Use in HTML:

 <tr ng-repeat="item in items" post-repeat-directive></tr> 


Features of chronology tracking using development tools in Chrome

On the timeline tab of the Chrome developer tools, you can see events (events), the number of browser frames per second (frames) and memory allocation (memory). The memory tool is useful for detecting memory leaks and for determining the amount of memory your application needs. Page flicker becomes a problem when the refresh rate is less than 30 frames per second. The frames tool shows information about the performance of the page display system. In addition, it displays how long the CPU consumes javascript.

')

Basic settings that limit 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 infinite scrolling.



Pagination


Our pagination method is based on a combination of the AngularJS limitTo filter (starting with version 1.1.4) and our startFrom filter. This approach allows to reduce the 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); }; 


Use in HTML.

 <!--     --> <button ng-repeat="i in getNumberAsArray(numberOfPages()) track by $index" ng-click="setCurrentPage($index)">{{$index + 1}}</button <!--   --> <tr ng-repeat="item in displayedItemsList | startFrom: currentPage * pageSize | limitTo:pageSize" /tr> 


If you do not want or can not use pagination , but you are still worried about the problem of slow filtering, do not be lazy to see step 5, and use ng-hide to hide unnecessary list items.



Infinite scrolling


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



Optimization recommendations



1. Display the 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 you don’t 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 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-repeat evaluates all expressions on each $ digest loop, i.e. this is done very often. In our example, filteredItems() returns a filtered collection. If it is slow, it will quickly slow down the entire application.

 <li ng-repeat="item in filteredItems()"> <!--,      .--> <li ng-repeat="item in items"> <!--     --> 


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 and the data list. This allows you to pre-apply multiple filters and apply collection caching in the view. The following example shows a very simplified implementation. The filteredLists variable represents the collection cache, and the applyFilter method applyFilter 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 the presentation:

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




4. Use ng-if instead of ng-show to supplement the 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-if disables the display (unlike ng-show ). In this case, additional elements are added, and bindings are resolved exactly when they are needed.

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


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


In our opinion, the use of the built-in AngularJS ng-mouseenter caused 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. Don't forget to just wrap mouse events with the jQuery.live () function — to receive notifications from elements added to the DOM later.



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


With long lists, filters also work more slowly, as each filter creates its own subset of the original list. In many cases, when the original 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 result of the filter at the moment it is needed, saving on processing time.

When applying filters with the ng-repeat directive, each filter returns a subset of the original collection. AngularJS also removes filter-excluded elements from the DOM, and raises the $destroy event, which also removes them from $scope . When the input collection changes, the subset of elements passing through the filter also changes, which again causes their redrawing or destruction.

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-show and ng-hide directives. Calculate the filters in the controller and add a property for each element. Use ng-show with the value of this property. As a result, the ng-hide directive will simply add a specific class, instead of removing items from a subset of the original collection, $scope and DOM.



7. Configure filtering hints: send 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 the search string, the filter needs to be simply activated after the user finishes 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; }; 


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


For further reading



  1. Project organization for huge applications
  2. Misko Hevery's answer to StackOverflow when asked about the performance of data bindings in Angular
  3. A short article on various ways to improve the performance of ng-repeat
  4. Download large data on demand
  5. Good article on scope
  6. AngularJS project for dynamic templates
  7. Display without data bindings

Source: https://habr.com/ru/post/200670/



All Articles