AngularJS 'ng-filter' is very slow on array of ~1000 elements
Asked Answered
C

6

38

I have a simple <input> search filter set up for a list of itemnames in AngularJS.

My list looks like this:

var uniqueLists = {
    category1: ['item1', 'item2', 'item3' ... 'item180' ], // Real list contains ~180 items
    category2: ['itemA', 'itemB', 'itemC' ... 'itemZZZ' ], // Real list contains ~1080 items
    category3: ['otheritem1', 'otheritem2', 'otheritem3' ]  // Real list contains 6 items
  }

I iterate through this list in Angular and print out the results in a <ul> for each category.

<div ng-repeat="(key,val) in uniqueLists">
    <form ng-model="uniqueLists[index][0]">
        <input ng-model="searchFilter" type="text" />
            <ul>
                <li ng-repeat="value in val | filter: searchFilter">
                    <label>
                         <input type="checkbox" ng-model="selectedData[key][value]" />
                        {{value}}
                    </label>
                </li>
            </ul>
    </form>
</div>

For clarity, selectedData looks like this:

var selectedData = {category1: [item1:true], category2: [], category3: []); // if 'item1's checkbox is checked.

This list is working just fine, although the filter is quite laggy, even on my quite-fast computer. Typing a letter into the input takes 1-2 seconds for the list to update.

I'm aware that this is likely because I'm filtering through around about 1000 items at a time, but I haven't seen any discussion of this elsewhere.

Is there any way to get better performance out of the filter?

Canned answered 31/7, 2013 at 11:34 Comment(1)
A jsfiddle with your code'll really help in this case. Also I can imagine that having a loop in a loop and then filter is quite much, u're also creating a form inside the first loop. A jsffidle'll really help us help you ;)Grazia
A
55

The main problem with the filter approach is that upon each change the dom is manipulated, so it's not the filter that's slow but the consequences. An alternative is to use something like:

ng-show="([item] | filter:searchFilter).length > 0"

on the repeated element.

Lending some code from @OverZealous, you can use the following to compare the behaviour:


Update: With Angular v1.2 came the track by syntax. Which also helps with such problems. Provided the elements have some unique attribute, one can use:

ng-repeat="item in items | filter:searchFilter track by item.id"

Where item.id has to be unique across all items. With track by only those dom-elements will be removed which are no longer the in the final list, others will be remembered. Whereas without track by the whole list is redrawn everytime. In short: much less dom manipulation = quicker redraw.

Aeniah answered 31/7, 2013 at 13:23 Comment(7)
This should be the answer. This worked so well i couldn't believe it!Motto
The speed improvement for ng-show is really impressive. Thank you for the explanationMoralez
Returning several years later, can't believe I forgot to mark an answer!Canned
i tried ur code with 50 000 items and 1. and 3. solution would block the chrome, however 2nd solution with ng-show was pretty fast and executed in half a secBannock
ng-repeat="item in items | filter:searchFilter track by item.id" This led to precisely 0 performance increase. My data was already in object form, with unique values, adding track by depot.val made absolutely no difference. Am I missing something?Sarmatia
Thanks for this one! Works really good for me as well! The difference in performance is huge between ng-show and filter.Girgenti
Thanks. It saved my day. Very very help ful.Courbet
K
21

Another interesting optimization is to 'not trigger' the model change until a certain time.

Adding this to your search input field: ng-model-options="{debounce: 500}"

This will trigger the filter if the user stop typing during 500ms.

I updated the above fiddle:

http://jsfiddle.net/CXBN4/14/

<input ng-model="searchFilter" type="text" ng-model-options="{debounce: 500}" />
Kuth answered 29/1, 2015 at 23:45 Comment(1)
If you want to use it in combination with ng-change have a look here: https://mcmap.net/q/179945/-angular-ng-change-delayNematode
H
5

I created a fiddle to simulate (part of) the code you are showing.

On my computer, which is fast but not super fast, that runs acceptably well. It's a little slow, but it's filtering an overly long list that has two-way binding with the checkboxes. Every time you type a letter, the entire list must be scanned and items removed (or added).

I think your best bet for solving this is to add some sort of simple pagination, as shown in this StackOverflow answer.

Here I've modified my example to include pagination. You will probably want to invest in a better solution than just Next / Previous, but this shows you how the result can be extremely fast if you aren't showing everything all at once. It still searches the whole list, but the rendered list is much more limited.

The additions:

Add paging info to the scope in the controller:

$scope.currentPage = 0;
$scope.pageSize = 20;
$scope.numberOfPages = function () {
    return Math.ceil($scope.items.length / $scope.pageSize);
}

Create a new filter to start from a specific page:

app.filter('startFrom', function () {
    return function (input, start, pageSize) {
        start = +start; //parse to int
        pageSize = +pageSize;
        while (start > input.length) {
            start -= pageSize;
        }
        if (start < 0) {
            start = 0;
        }
        return input.slice(start);
    };
});

Add filters to the view to limit the list:

<li ng-repeat="value in items | filter:searchFilter |
        startFrom:currentPage*pageSize:pageSize | limitTo:pageSize">

Add pagination buttons to the page:

    <div>
        <button ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">Previous</button> {{ currentPage+1 }}/{{ numberOfPages() }}
        <button ng-disabled="currentPage >= items.length/pageSize - 1" ng-click="currentPage=currentPage+1">Next</button>
    </div>
Heartfelt answered 31/7, 2013 at 12:56 Comment(0)
L
4

Every time you press key in input all watch expressions need to be executed and looking at your code you have lot of them. If item names are immutable you could use for example https://github.com/abourget/abourget-angular which allows you to write:

<label>
     <input type="checkbox" ng-model="selectedData[key][value]" />
     <span set-text='value'></span>
</label>

And you have 1000 less watch expressions to execute on each keystroke.

Additionally you could use some kind of throttling on input so filter triggers after 500ms from last keystroke.

Lard answered 31/7, 2013 at 12:53 Comment(0)
H
4

You could also limit the items that will be displayed using a "limitTo" filter. This allows you to still have a huge number of items in your model that you are filtering out, but it won't be as slow because you're not trying to show ALL the items in the DOM.

Here is a modified jsbin based on earlier answers, but with the limitTo filter applied:

http://jsbin.com/IhOcaKo/1

Hurter answered 14/10, 2013 at 19:25 Comment(0)
M
1

No solutions work for me :(

Finally I SOLVED the problem this way simply:

<li ng-repeat="value in val | filter: searchFilter | limitTo:200">

just try it and get solved... :)

Microgamete answered 21/6, 2017 at 7:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.