Mathematics
Mathematics

Reputation: 7628

How to deal with browser freezing because of nested ng-repeat

I created a nested tree which may have 1 - 5000 items, I am able to make it work but it freezes my browser\loading spinner for few seconds just before showing the tree.

How can I make it smooth so browser would never freeze ?

How can I know when angularjs finished creating or rendering or computing (not sure right word) whole list so that I could remove loading spinner then, as you can see scope.$last won't work as we have nested ng-repeat and same for scope.$parent.$last

Here is plunker I created but with demo data -

http://plnkr.co/edit/GSZEpHjt5YVxqpg386k5?p=preview

Example dataset - http://pastebin.com/YggqE2MK

It's not too bad in this example but at points my browser freezes for more then 10 seconds for around 4000 items in my OWN setup with all other components.

What I already considered

HTML

  <script type="text/ng-template" id="tree_item">
    <div ng-init="category.expanded=true">
      <div ng-class="{'selected': category.ID==selectedCategoryID}">
        <div class="icon icon16" ng-if="category.Children.length>0" ng-click="$parent.category.expanded=!$parent.category.expanded" ng-class="$parent.category.expanded?'col':'exp'"></div>
        <div class="icon icon-category" ng-class="'icon-category-'+category.TypeType" ng-style="{'border-color':category.Colour}" ng-attr-title="{{category.Status?Res['wpOPT_Status'+category.Status]:''}}"></div>
        <a ng-href="#id={{category.ID}}" ng-class="{'pending-text': category.PendingChange}">{{category.Name}}</a>
      </div>
      <ul class="emlist" ng-show="category.expanded">
        <li ng-repeat="category in category.Children | orderBy:'Name'" ng-include="'tree_item'" ng-class="{'selected': category.ID==selectedCategoryID}" e2-tree-item is-selected="category.ID==selectedCategoryID">
        </li>
      </ul>
    </div>
  </script>

  <div id="CategoryListContainer" class="dragmenu-container initial-el-height">
    <div class="spinner" data-ng-show="status=='loading'"></div>
    <ul id="CategoryList" class="dragmenu-list ng-cloak" ng-show="status=='loaded'">
      <li ng-repeat="category in tree | orderBy:'Name'" ng-include="'tree_item'" e2-tree-item is-selected="category.ID==selectedCategoryID"></li>
    </ul>
  </div>

JS

var app = angular.module('recursionDemo', []);

app.controller("TreeController", function($scope, $timeout) {
  $scope.status = "loading";

  $timeout(function() {
        var result = {
      "GetCategoryTreeResult": [{ // too big data set so I pasted it here http://pastebin.com/YggqE2MK }];
    $scope.tree = result.GetCategoryTreeResult;

    $scope.status = "loaded";
  }, 3000);
});

app.directive('e2TreeItem', function($timeout) {
  function link(scope, element, ngModel) {
    scope.$watch('isSelected', function(oldVal, newVal) {
      if (scope.isSelected === true) {
        element.parentsUntil('#CategoryListContainer', 'li').each(function(index, item) {
          angular.element(item).scope().category.expanded = true;
        });
      }
    });

    // not working
    //if (scope.$parent.$last) {
    //  console.log("last has been caught");
    //  var appElement = document.querySelector('[ng-app=recursionDemo]');
    //  angular.element(appElement).scope().status = "loaded";
    //}
  }
  return {
    link: link,
    scope: {
      isSelected: '=?'
    }
  };
});

Upvotes: 14

Views: 8688

Answers (3)

Matej Marconak
Matej Marconak

Reputation: 1413

From my experience:

Also this is old, but good article on angularjs performance with ng-repeat Optimizing AngularJS: 1200ms to 35ms

Upvotes: 0

blackmiaool
blackmiaool

Reputation: 5344

UPDATE:

DEMO:http://plnkr.co/edit/bHMIU8Mx5grht9nod1CW?p=preview

You can load your data smoothly by changing the L10901 of app.js as below. But you should notice that if you still want to order it by name, and want it steady, you should just sort the source data, but not sort in angular with "order by". (Do you want some code to sort the source data?)

    function getDataGradually(data, childKey, gap, count, updateCb, finishCb) {
        const lastPositon = [];
        const linearData = [];
        const ret = [];


        function getLinearData(arr, position) {
            arr.forEach(function (obj, index) {
                const pos = position.concat([index]);
                if (obj[childKey] && obj[childKey].length) {
                    var children = obj[childKey];
                    obj[childKey] = [];
                    linearData.push({
                        obj,
                        pos
                    });
                    getLinearData(children, pos);
                } else {
                    linearData.push({
                        obj,
                        pos
                    });
                }
            });
        }
        getLinearData(data, []);

        function insertData({
            obj,
            pos
        }) {

            let target = ret;
            pos.forEach(function (i, index) {


                if (index === pos.length - 1) {
                    target[i] = obj;

                } else {
                    target = target[i][childKey];
                }
            });

        }
        let handled = 0;

        function doInsert() {
            let start = handled;
            let end;
            if (handled + count > linearData.length) {
                end = linearData.length;
            } else {
                end = handled + count;
            }
            for (let i = start; i < end; i++) {
                insertData(linearData[i]);
            }
            handled += count;
            if (handled < linearData.length) {
                setTimeout(function () {
                    doInsert();
                    updateCb && updateCb();
                }, gap);
            } else {
                finishCb && finishCb();
            }
        }
        doInsert();



        return ret;
    }

    $scope.tree = getDataGradually(result.GetCategoryTreeResult, "Children", 0, 30, function () {
      $scope.$digest();
    }, function () {
      //finished 
    });
    //$scope.tree = result.GetCategoryTreeResult;
    $scope.status = "loaded";
  }, 3000);

Old answer:

You can get the rendering time by using $timeout. Try to change the L10901 of app.js as below.

        $scope.tree = result.GetCategoryTreeResult;
        console.time("A")
        $timeout(function(){
          console.timeEnd("A");
        });
        $scope.status = "loaded";
      }, 3000);

BTW, I got "1931.881ms" on my pc.

Upvotes: 4

Aks1357
Aks1357

Reputation: 1072

The way I handle this is:

In Controller :

// Consider results to be my main array, which contains 5000 items
var results = [{ ... }, { ... }, ..., ..., { ... }];

$scope.bindResults = results.slice(0, 5);

// Here you can start loading
$scope.status = "loading";

// Inject $interval in controller
var interval = $interval(function() {
    $scope.bindResults = results.slice(0, $scope.bindResults.length + 5);

    if($scope.bindResults.length >= results.length) {
         $interval.cancel(interval);

        // Here you can end loading
        $scope.status = "loading";
    }
}, 10);

In HTML :

<span ng-repeat="x in bindResults"></span>

This won't hang up the browser and will keep populating data every 10 ms, you can increase that number as required.

Upvotes: 0

Related Questions