1

I have a list of 1000+ items which I display using NgRepeat in Angular 1.3. The list populates with buttons. I have noticed significant delay on the click event int he list once it grows in size. When the list is only 5-10 items the clicks are instant. When the list is 1000 there is about 2-5 second delay before the button clicks are actually processed.

Now I cannot tell if this is a browser issue, but I suspect it has to do with too many listeners being used somewhere, causing the browser to check for them.

Here is sample of code in case there is a culprit hiding in there:

<div id="side" class="animated" style="min-height: 250px;" 
    data-ng-class="{'fadeInRight':documentDone}" data-ng-style="settings.listCss">
    <div class="col-md-12 text-center" data-ng-style="settings.listCss"><h4>{{label}}</h4> {{inSide}} </div>
    <div data-ng-repeat="doc in ::documents track by $index" id="{{ ::doc.id }}" 
        class="document ng-hide" data-ng-show="doc.show"
        data-ng-init="docSettings = (settingslist[doc.companyid] || settings)" data-ng-style="::docSettings.listCss">
        <div class="col-md-12" data-ng-style="docSettings.listCss">
            <h4>
                <span>{{ ::$index + 1 }}</span>
                <span class="title-in-clusters">
                    {{ ::doc.title }}
                    <button type="button" 
                        class="btn btn-primary btn-xs" 
                        data-ng-click="viewJob(doc, docSettings)" 
                        data-ng-style="docSettings.buttonCss">
                        <strong>VIEW</strong>
                    </button>
                    <a href="{{ ::doc.joburl }}" class="apply" target="_blank">
                        <button type="button" class="btn btn-primary btn-xs" data-ng-click="apply(doc.jobid, doc.companyid)" 
                            data-ng-style="docSettings.buttonCss">
                            <strong>APPLY</strong>
                        </button>
                    </a>
                </span>
            </h4>
        </div>
        <div class="col-md-12" data-ng-style="docSettings.listCss">
            <span class=""><strong>ID: </strong>{{ ::doc.jobid }}</span>
            <img data-ng-if="docSettings.heading.logourl && docSettings.heading.logourl != ''" 
                data-ng-src="{{docSettings.heading.logourl}}" class="side-logo inline-block" id="">
        </div>
        <div class="col-md-12" data-ng-style="docSettings.listCss">
            <strong>Location: </strong><span class="">{{ ::doc.location }}</span>
        </div>                  
        <div class="col-md-12" data-ng-style="docSettings.listCss">
            <strong>Updated Date: </strong><span class="">{{ ::doc.updateddate }}</span>
        </div>
        <div class="col-md-12" data-ng-style="docSettings.listCss">
            <hr data-ng-style="docSettings.listCss">
        </div>
    </div>
</div>

There is nothing offensive about the other functions that are called when the button is pressed.

var modalInstance;
$scope.viewJob = function(modalDoc, docSettings) {
    $scope.modalDoc = modalDoc;
    $scope.docSettings = docSettings;
    //the trusAsHtml takes string creates an object, so this will in essence convert string to object
    //make sure you check if it is a string since it could be called multiple times by user (close and reopen same modal)
    if (modalDoc.overview && typeof modalDoc.overview === 'string') {
        $scope.modalDoc.overview = $sce.trustAsHtml(modalDoc.overview);
    }
    if (modalDoc.qualifications && typeof modalDoc.qualifications === 'string') {
        $scope.modalDoc.qualifications = $sce.trustAsHtml(modalDoc.qualifications);
    }
    if (modalDoc.responsibilities && typeof modalDoc.responsibilities === 'string') {
        $scope.modalDoc.responsibilities = $sce.trustAsHtml(modalDoc.responsibilities);
    }

    modalInstance = $modal.open({
      templateUrl: 'app/modal/job_preview.html',
      //templateUrl: 'myModalContent.html',
      scope: $scope
    });
  };

I want to optimize this code so it can sever a list of up to 1500, but I cannot for the life of me find the culprit.

I will also take any solutions to reduce the load instead. Like for now I am thinking I may limit the number of DOM elements to 10 to so, and have angular rotate what is being viewed as user scrolls if it will result in better UX.

UPDATE:

Many things have been tried, from use of bind-once to more convoluted solutions that retard some of the watchers Which are enat but require a lot of Math to estimate which items are visible etc.

I finally decided on one solution that was easiest to do: I made a list of only items I wish shown and on mouse scroll up or down I edit the list.

First part of the solution is use of two directives:

.directive('ngMouseWheelUp', function() {
    return function($scope, $element, $attrs) {
        $element.bind("DOMMouseScroll mousewheel onmousewheel",     
        function(event) {
                    // cross-browser wheel delta
                    var event = window.event || event; // old IE support
                    var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));

                    if(delta > 0) {
                        $scope.$apply(function(){
                            $scope.$eval($attrs.ngMouseWheelUp);
                        });

                        // for IE
                        event.returnValue = false;
                        // for Chrome and Firefox
                        if(event.preventDefault) {
                            event.preventDefault();                        
                        }

                    }
        });
    };
})
.directive('ngMouseWheelDown', function() {

    return function($scope, $element, $attrs) {

        $element.bind("DOMMouseScroll mousewheel onmousewheel", function(event) {

                    // cross-browser wheel delta
                    var event = window.event || event; // old IE support
                    var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));

                    //console.log(event);

                    if(delta < 0) {
                        $scope.$apply(function(){
                            $scope.$eval($attrs.ngMouseWheelDown);
                        });

                        // for IE
                        event.returnValue = false;
                        // for Chrome and Firefox
                        if(event.preventDefault)  {
                            event.preventDefault();
                        }

                    }
        });
    };
})

These two enable me to disable scrolling in the list on the right side. Then I would create two additional arrays from the documents in routeScope. First list would be generated whenever the documents were updated (which was an event listener for event emitted by the UI of the right hand side graph), this filter would only return array members that had the show property set to true:

var showFilter = function(object) {
    return object.show;
}

This would be my array of visible items. From this array I created another Array of shown items. I defined a constant for max size of 7, so at most there are 7 items shown. And of course I set overflow of the parent container to none to disable scrollbar. (I may add a scroll graphic so the user knows he can scroll this field later)

Then I added the following directives to the side div: data-ng-mouse-wheel-up="listUp()" data-ng-mouse-wheel-down="listDown()" And inside the controller I defined listUp and listDown to work off an index and the max size constant to figure out which elements from the visible list I should add to the front or the back of the shown list.

/**
 * Simulate scrolling up of list by removing bottom element and adding to top
 */ 
$scope.listUp = function() {
    $rootScope.shownDocuments.unshift(getPrev());
    $rootScope.shownDocuments.pop();
}
/**
 * Simulate scrolling down of list by removing top element and adding to bottom
 */
$scope.listDown = function() {
    $rootScope.shownDocuments.push(getNext());
    $rootScope.shownDocuments.shift();
}
/**
 * return next item in visibleDocuments array
 */
var getNext = function() {
    $rootScope.topIndex++;
    if ($rootScope.topIndex > $rootScope.visibleDocuments.length) {
        $rootScope.topIndex -= $rootScope.visibleDocuments.length;
    }
    return ($rootScope.visibleDocuments[($rootScope.topIndex+max_shown_size)%$rootScope.visibleDocuments.length]);
}
/**
 * Return previous item in visibleDocuments array
 */
var getPrev = function() {
    $rootScope.topIndex--;
    if ($rootScope.topIndex < 0) {
        $rootScope.topIndex += $rootScope.visibleDocuments.length;
    }
    return ($rootScope.visibleDocuments[$scope.topIndex]);
}

Use of rootScope vs scope is mostly because modals would cause some undesirable behaviors if they were dismissed improperly.

Finally a reset function for the view:

/**
 * Resets the list of documents in the visibleList (IE which are visible to client)
 */
var updateVisibleDocuments = function() {
    $rootScope.topIndex = 0;
    $rootScope.visibleDocuments = $rootScope.documents.filter(showFilter);
    //clear view
    $rootScope.shownDocuments = [];
    $rootScope.topIndex = 0;
    for (var i = 0; i < max_shown_size; i++) {
        $rootScope.shownDocuments.push(getNext());
    }
    $rootScope.topIndex = 0;
}

This solution works really well because I only render 7 items even if my list has 100k items. This limits number of watchers tremendously.

  • do you have sample data? or could make jsfiddle or plunker? there should be some way to improve that. – YOU Aug 02 '15 at 08:36
  • Im working on plunker, but I am having hard time replicating the issue there. I have encolsed a link to actual product site. http://job-portal.rightfit.it/#/company/comrise – Witold Cyrek Aug 03 '15 at 19:50
  • 1
    And if you want to see drop in performace visit this link http://job-portal.rightfit.it/#/company/ADP After 300 it's becoming very noticeable. – Witold Cyrek Aug 03 '15 at 19:52
  • with 18k $watchers, I won't surprise it slow, and also looks like developement build. downloading js files took 34 seconds in in my computer and angular took like 10s. I will make a fiddle to faster alternative on this weekend, and will post it. I will use some library that can improve that. – YOU Aug 04 '15 at 14:09
  • 1
    Thanks. I got a new chrome extension that keeps track of number of watchers without crashing, and I noted that, too. I'm trying to find the bottleneck while I have the time. Originally I never expected the list to exceed 200 so it was good enough, but the project took an escalated route. – Witold Cyrek Aug 04 '15 at 14:39
  • one quick tip: move ng-controller="ApplyController" outside ng-repeat, so angular won't need to initialize controller 1000+ times. – YOU Aug 04 '15 at 15:11
  • I extract that ng-repeat part in put it in a [gist](https://gist.github.com/S-YOU/8907b8b7b14bb56b3229#file-jobview1-html), it does not slow if you run locally (download and run jobview1.html). problem is data, it is 4.7M. may be you don't need to load detail data at once, but only load when user click VIEW. – YOU Aug 05 '15 at 14:13
  • Thanx. The first tip is already done on dev server. I also had no problem when running something similar in [plnkr](http://plnkr.co/edit/8W408VyNNpNWDCIvBEPh?p=preview) But the number of watchers was almost identical. Even if I use bind-once on everything that will remain static. I'll see if the data can be minimized as well. – Witold Cyrek Aug 05 '15 at 21:09
  • great, looks like you re-implemented something like angular-vs-repeat yourself. – YOU Aug 10 '15 at 02:28

1 Answers1

0

You may want to try paginating to reduce the amount of things angular and the browser need to deal with on screen at any one time.

Sina Khelil
  • 2,001
  • 1
  • 18
  • 27
  • Thanks! The issue with pagination in this case is that the list stays static but the displayed results change based on a filter that you click in an interactive diagram. So i can hide part of the list, but I need the whole list in memory because I don't want to make callbacks to the server for more data. Unless I can do the pagination with javascript on front end, which would work in most cases but... – Witold Cyrek Jul 27 '15 at 17:39
  • The list on the right side is made to occupy 100% of the height of the viewport, so I would have to estimate the height before I decide how many items to show per page, otherwise I end up with a scroll bar and page button, which would be totally horrible UX. Not saying I wont do that, just saying this is what I would have to consider. – Witold Cyrek Jul 27 '15 at 17:42
  • so, to troubleshoot, have you looked to see in the profiler what is taking the most time? what does the docsettings(service?) do? I do not see anything killer except for the sheer number of items. – Sina Khelil Jul 27 '15 at 17:48
  • I am not that well versed with the profiler to get much useful information out of it. You can run profiler yourself at this site: http://job-portal.rightfit.it/#/company/ADP click on "VIEW" button while you have it open to see the issue. – Witold Cyrek Aug 04 '15 at 13:03