Click here to Skip to main content
13,150,622 members (45,005 online)
Click here to Skip to main content
Add your own
alternative version

Stats

7K views
2 bookmarked
Posted 5 Mar 2017

ng-repeat performance degradation at case of very big iterated array

, 13 Sep 2017
Rate this:
Please Sign up or sign in to vote.
How to solve problem, when ng-repeat directive iterates very big array and it cause significant performance degradation.

Problem

Probably, you may had situation, when used ng-repeat directive to iterate very big array with the length more than 2k or 3k records with very complicated presentation logic of each item, I mean you may show it's several properties, change style, show or hide some html tags and so on, depending on some conditions. At this case, you can face performance degradation problem, because AngularJS creates a lot of watchers with total number proportional to the array.length.

Solution

First of all, we can say, that if you have more than 2k records, you probably won't show all of them together on screen, you will use some scrolling, for example infinite scrolling or usual one. Let's consider last situation. At this case, we can tell AngularJS to show only certain portion(window) of all records, so instead of thousands rows, that should be processed and rendered, we will have strictly defined constant quantity, that means the number of watchers also will be limited and it will solve our performance problem.

Fortunately, AngularJS supply as with feature limitTohttps://docs.angularjs.org/api/ng/filter/limitTo. With it's help we can specify what portion of array will be shown at current moment of time:

<code>{{ limitTo_expression | limitTo : limit : begin}}</code>

Where limit -  size of portion and begin - from which array's index portion will be started. So all what we should do - to change begin parameter according to scroll position. For this task we will create smartScroll directive:

.directive("smartScroll", function() {
    return {
      restrict: 'E',
      scope: {
        to: '=',
        length: '@'
      },
      template: `
                <div class="smart-scroll" style="overflow:auto;">
                  <div></div>
                </div>
            `,
      link: function(scope, element, attrs) {
        //set height of root div
        var root = angular.element(element.find('div')[0]);
        root.css('height', attrs.height);

        //scrolling over table also will work
        if (attrs.baseid) {
            var baseEl = document.getElementById(attrs.baseid);
            var handler = function(event) {
                element[0].firstElementChild.scrollTop += event.deltaY;
                event.preventDefault();
            }

            baseEl.addEventListener("wheel", handler);

            scope.$on('$destroy', function () {
                baseEl.removeEventListener("wheel", handler);
            });
        }

        scope.$watch('length', function() {
          //when array.length is changed we will change height of inner div 
          //to correct scrolling presentation of parent div accordingly
          var height = (scope.length - attrs.limit) * attrs.sens + attrs.height * 1;
          angular.element(element.find('div')[1]).css('height', height);

          //if we won't need scrolling anymore, we can hide it 
          //and shift scrolling to initial top position
          if (scope.length <= attrs.limit) {
            root[0].scrollTop = 0;
            root.css('display', 'none');
            scope.to = 0;
          } else
            root.css('display', 'block');
        });

        //when we perform scrolling, we should correct "to" argument accordingly
        root.on('scroll', function(event) {
          var scrolled = root[0].scrollTop;
          scope.$apply(function() {
            scope.to = scrolled / attrs.sens;
          });
        });
      }
    };
  });

HTML usage:

<tbody id='tableBody'>
    <tr ng-repeat="item in vm.array | limitTo : 10 : vm.to">   
       <td>{{item.name}}</td> 
       <td>{{item.age}}</td> 
    </tr> 
</tbody>

<smart-scroll sens='10' limit='10' height='400' length='{{vm.array.length}}' to='vm.to' baseid='tableBody'>
</smart-scroll>

Let's consider parameters:

  1. sens - this argument is opposite to sensitivity of scrolling, so 1 means very big sensitivity.
  2. limit - size of portion, limit part of limitTo
  3. height - simple corresponding html style attribute for root directive
  4. length - length of array (is watched)
  5. to - current position, from which portion is started (is changed), begin part of limitTo

Directive consists of two div tags: one with the height specified as parameter and with scrolling capability and second which located inside former. Scroll position of root div will have two way data binding with to parameter: the greater the scroll pulls down the more to parameter should become and vice versa. Scrolling range depends on only height of inner div, because corresponding property of root div is a constant value. Height of inner div should be proportional to the (array.length - limit) i.e. (scope.length - attrs.limit), because we should be able to show all records, also we provide very important and strictly accordance: sens * 1 pixelOfScrolling = 1 record, so to = ScrollPosition / (sens * 1 pixelOfScrolling) i.e. scope.to = scrolled / attrs.sens.

So height of inner div regulates scrolling range, which is proportional to the array.length and scroll position of root div connected with to parameter i.e. what portion of records (from what index of array) should be shown.

We still have one problem: each time we intend to scroll mouse over table, nothing will happen, because our custom scrolling is not a native part of table. To fix it we will pass id of data container(table or tbody tag) via baseid attribute to our directive, then add event listener on wheel event on this element and transpile it to custom scrollling and prevent former.

I created sample with all of this code: https://plnkr.co/edit/TuRTIpplK4eLus2NVd4D. At this example I also add filter on array, so you can show not only some portion of original array, but also from filtered one with some conditions. It can be very comfortable to combine them together. Also you should tune some CSS, to make directive works properly at all browsers.

I will reproduce this example:

HTML:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <script src="//code.angularjs.org/snapshot/angular.min.js"></script>
  <script src="app.js"></script>

    <!-- [if IE] >
        <style>        
            .smart-scroll {                 
                position: relative;
            }        
        </style>
    <![endif]-->
    <!--[if !IE]><!-->
        <style>
            .smart-scroll {                
                width:15px;
            }
            @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
                .smart-scroll {
                    position: relative;
                    width:auto;
                }
            }
        </style>
    <!--<![endif]-->    
    <style type="text/css">
        @-moz-document url-prefix() {
            .smart-scroll {
                position: relative;
                width: auto;
            }
        }
    </style>

</head>

<body ng-app="app">
  <div ng-controller="MyController as vm">

    <div class="row">
      <div class="col-sm-8">
        <div class="form-group">
          <label>Search</label>
          <input class="form-control" ng-model="vm.search" />
        </div>
      </div>
      <div class="col-sm-2">
        <div class="form-group">
          <label>&nbsp;</label>
          <button type="button" class="btn btn-success col-sm-12" ng-click="vm.add()">Add to Top</button>
        </div>
      </div>
    </div>

    <div class='row'>
      <div class="col-sm-10">
        <table class="table table-bordered" style="margin-bottom:0">
          <thead>
            <tr>
              <th>Name</th>
              <th>Age</th>
              <th></th>
            </tr>
          </thead>
          <tbody ng-init="vm.to=0" id='tableBody'>
            <tr ng-repeat="item in vm.getItems() | limitTo : 10 : vm.to" ng-style='{"background-color": item.add ? "#ccffcc" : "white"}'>
              <td>{{item.name}}</td>
              <td>{{item.age}}</td>
              <td>
                <a ng-click="vm.removeItem(item)" href='#'>X</a>
              </td>
            </tr>
          </tbody>
          <tr>
            <th>Total:</th>
            <th colspan='2'>{{vm.quantity}}</th>
          </tr>
        </table>
      </div>
      <div class="col-sm-1" style="margin-top:0;padding-left: 0">
        <smart-scroll sens='10' limit='10' height='400' length='{{vm.quantity}}' to='vm.to' baseid='tableBody'></smart-scroll>
      </div>
    </div>

  </div>
</body>

</html

Javascript:

(function(angular) {
  'use strict';
  var myApp = angular.module('app', []);

  myApp.controller('MyController', ['$scope', '$filter', function($scope, $filter) {
    var self = this;

    self.filter = $filter('filter');
    self.items = [];
    self.quantity = 50000;
    for (var i = 0; i < self.quantity; i++)
      self.items.push({
        name: 'Name' + i,
        age: i + 1
      });

    self.getItems = function() {
      var out = self.filter(self.items, {name : self.search});
      self.quantity = out.length;
      return out;
    };

    self.removeItem = function(item) {
      self.items.splice(self.items.indexOf(item), 1);
    };

    self.add = function() {
      self.items.unshift({
        name: 'Name' + self.items.length,
        age: i + self.items.length,
        add: true
      });
    };

  }]).directive("smartScroll", function() {
     //directive's code is already presented above
  });
})(window.angular)

Conclusions

At this article I showed how to solve problem with performance degradation at case of usage of ng-repeat with very big iterated array. Solution is based on idea to show and render only visible for user portion of this array instead of whole one. This goal can be archived by using simple scrolling with AngularJS feature: limitTo and implemented as custom directive, where position of scrolling is reflected to array's index which is a top border of showed portion(window).

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

SlavaUtesinov
Russian Federation Russian Federation
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
Questionprovide a jsfiddle version of your code Pin
Mou_kol7-Sep-17 22:55
memberMou_kol7-Sep-17 22:55 
AnswerRe: provide a jsfiddle version of your code Pin
SlavaUtesinov10-Sep-17 19:26
memberSlavaUtesinov10-Sep-17 19:26 
GeneralRe: provide a jsfiddle version of your code Pin
Mou_kol10-Sep-17 22:56
memberMou_kol10-Sep-17 22:56 
GeneralRe: provide a jsfiddle version of your code Pin
SlavaUtesinov11-Sep-17 2:44
memberSlavaUtesinov11-Sep-17 2:44 
GeneralRe: provide a jsfiddle version of your code Pin
Mou_kol12-Sep-17 2:28
memberMou_kol12-Sep-17 2:28 
GeneralRe: provide a jsfiddle version of your code Pin
SlavaUtesinov13-Sep-17 1:58
memberSlavaUtesinov13-Sep-17 1:58 
GeneralRe: provide a jsfiddle version of your code Pin
Mou_kol14-Sep-17 3:14
memberMou_kol14-Sep-17 3:14 
GeneralRe: provide a jsfiddle version of your code Pin
SlavaUtesinov14-Sep-17 3:18
memberSlavaUtesinov14-Sep-17 3:18 
GeneralRe: provide a jsfiddle version of your code Pin
Mou_kol17-Sep-17 23:03
memberMou_kol17-Sep-17 23:03 
GeneralRe: provide a jsfiddle version of your code Pin
SlavaUtesinov18-Sep-17 0:51
memberSlavaUtesinov18-Sep-17 0:51 
Questionstuck to understand a area Pin
Mou_kol7-Sep-17 22:55
memberMou_kol7-Sep-17 22:55 
AnswerRe: stuck to understand a area Pin
SlavaUtesinov10-Sep-17 19:30
memberSlavaUtesinov10-Sep-17 19:30 
GeneralRe: stuck to understand a area Pin
Mou_kol10-Sep-17 22:50
memberMou_kol10-Sep-17 22:50 
GeneralRe: stuck to understand a area Pin
SlavaUtesinov11-Sep-17 2:37
memberSlavaUtesinov11-Sep-17 2:37 
GeneralRe: stuck to understand a area Pin
Mou_kol12-Sep-17 2:26
memberMou_kol12-Sep-17 2:26 
GeneralRe: stuck to understand a area Pin
SlavaUtesinov12-Sep-17 23:52
memberSlavaUtesinov12-Sep-17 23:52 
Suggestionthis hard to be applicable in real project Pin
tranthanhtu.vn6-Mar-17 3:03
professionaltranthanhtu.vn6-Mar-17 3:03 
GeneralRe: this hard to be applicable in real project Pin
SlavaUtesinov6-Mar-17 21:01
memberSlavaUtesinov6-Mar-17 21:01 
AnswerRe: this hard to be applicable in real project Pin
tranthanhtu.vn6-Mar-17 21:23
professionaltranthanhtu.vn6-Mar-17 21:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.170924.2 | Last Updated 13 Sep 2017
Article Copyright 2017 by SlavaUtesinov
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid