13

I have an autocomplete form where the user can type in a term and it hides all <li> elements that do not contain that term.

I originally looped through all <li> with jQuery's each and applied .hide() to the ones that did not contain the term. This was WAY too slow.

I found that a faster way is to loop through all <li> and apply class .hidden to all that need to be hidden, and then at the end of the loop do $('.hidden').hide(). This feels kind of hackish though.

A potentially faster way might be to rewrite the CSS rule for the .hidden class using document.styleSheets. Can anyone think of an even better way?

EDIT: Let me clarify something that I'm not sure too many people know about. If you alter the DOM in each iteration of a loop, and that alteration causes the page to be redrawn, that is going to be MUCH slower than "preparing" all your alterations and applying them all at once when the loop is finished.

Nick
  • 5,228
  • 9
  • 40
  • 69
  • 1
    can you add in question what is the condition, you may just use css if it is not that complex – Milan Jaric Aug 12 '12 at 19:48
  • 1
    This is a bit odd because it should be faster to loop through the items and do .hide() on the ones you want to hide rather than loop through, add a class and then hide all the ones with that class. So something is messed up with your actual code. You will have to disclose the actual code for us to offer accurate advice. Also, you will need to test in multiple browsers with something like jsperf to come to accurate performance conclusions in multiple browsers. – jfriend00 Aug 12 '12 at 19:51
  • @jfriend00 The problem probably stems from calling `$` thousands of times. – Dennis Aug 12 '12 at 20:01
  • 1
    Why would you need to `hide()` all the `.hidden` elements? Shouldn't that be already defined in your stylesheet? – Zhihao Aug 12 '12 at 20:01
  • @jfriend00, the .hide() causes a page reflow thousands of times. Doing it once it surely faster. – Nick Aug 12 '12 at 20:02
  • @Zhihao, if it is already defined in the stylesheet, each time I addClass('.hidden') it will cause a page reflow. I only want to do that reflow ONCE at the end of everything. It's the same reason it's quicker to concatenate DOM elements and append them to the page at once instead of in each loop. – Nick Aug 12 '12 at 20:03
  • 1
    What I would do is to have an optimized search data structure, search that and create elements dynamically from the results rather than manipulating a huge dom tree all the time. This way you will only ever have `maxSearchResults` amount of lis at a time... – Esailija Aug 12 '12 at 20:04
  • @Esailija, that's kind of what I'm leaning toward after thinking about it a bit more. I'm glad you confirmed what I was thinking. Is it fast enough to delete and recreate chunks of DOM elements on each key press though? – Nick Aug 12 '12 at 20:08
  • @Nick: [Yes](http://jsfiddle.net/josh3736/uaSMq/). – josh3736 Aug 12 '12 at 20:11
  • @Dennis - that's why I said we have to see the ACTUAL code in order to diagnose this. There are enormously inefficient ways to use jQuery and there are efficient ways. It's pointless to ask us to diagnose a performance problem without disclosing the code. – jfriend00 Aug 12 '12 at 21:00
  • @Nick - `$('.hidden').hide()` is just calling `.hide()` a whole bunch of times in a loop too - there's no magic in jQuery. We have to see the ACTUAL code to diagnose a performance issue. – jfriend00 Aug 12 '12 at 21:01
  • @Nick - most decent browsers don't reflow thousands of times when you hide thousands of elements in one loop. They wait until the JS execution loop finishes and then do one reflow. I doubt this is a reflow issue. More likely really inefficient jQuery being used. – jfriend00 Aug 12 '12 at 21:04
  • @jfriend00 Browsers can only optimize things they can't make assumptions about. In order to optimize `hide()` calls in a loop, it would have to assume the user doesn't care about progressively hiding things (there may be a `sleep()` function after each `hide()` for instance and the results of the previous hide call must show on the screen). I amended the `hide()` in jQuery's source with my own counting function, and you are actually wrong -- it calls and reflows the page once on each loop iteration. – Nick Aug 12 '12 at 22:44
  • @Nick, there is no `sleep()` in javascript . The browser knows when the JS execution thread is running vs. done and it's single thread so it's simple and the browser is under no obligation to update the screen until the JS thread is finished running. No browser I know of causes layout between successive `hide()` calls unless you refer to a specific CSS property between `hide()` calls that requires layout to be refreshed in order to obtain that property accurately. I can do 1000 `hide()` calls extremely fast with no refresh between them when written properly. – jfriend00 Aug 12 '12 at 23:02
  • @jfriend00 Yes, I know there is no `sleep()` in Javascript. That was just an example -- a placeholder for anything that consumes time. I would have thought the context of the sentence implied that. You say "No browser I know of causes layout between successive `hide()` calls unless you refer to a specific CSS property between `hide()` calls that requires layout to be refreshed in order to obtain that property accurately". Well check out this: http://jsfiddle.net/GekPY/ No reference to any CSS properties and it redraws the page every for loop. – Nick Aug 13 '12 at 00:20
  • @Nick - you put an alert in your script. Of course that redraws on the alert because javascript execution is stopped. Take out the alert and you don't see the intervening `.hide()` operations: http://jsfiddle.net/jfriend00/fVPz7/. – jfriend00 Aug 13 '12 at 01:49

7 Answers7

25

Whenever you're dealing with thousands of items, DOM manipulation will be slow. It's usually not a good idea to loop through many DOM elements and manipulate each element based on that element's characteristics, since that involves numerous calls to DOM methods in each iteration. As you've seen, it's really slow.

A much better approach is to keep your data separate from the DOM. Searching through an array of JS strings is several orders of magnitude faster.

This might mean loading your dataset as a JSON object. If that's not an option, you could loop through the <li>s once (on page load), and copy the data into an array.

Now that your dataset isn't dependent on DOM elements being present, you can simply replace the entire contents of the <ul> using .html() each time the user types. (This is much faster than JS DOM manipulation because the browser can optimize the DOM changes when you simply change the innerHTML.)

var dataset = ['term 1', 'term 2', 'something else', ... ];

$('input').keyup(function() {
    var i, o = '', q = $(this).val();
    for (i = 0; i < dataset.length; i++) {
        if (dataset[i].indexOf(q) >= 0) o+='<li>' + dataset[i] + '</li>';
    }
    $('ul').html(o);
});

As you can see, this is extremely fast.


Note, however, that if you up it to 10,000 items, performance begins to suffer on the first few keystrokes. This is more related to the number of results being inserted into the DOM than the raw number of items being searched. (As you type more, and there are fewer results to display, performance is fine – even though it's still searching through all 10,000 items.)

To avoid this, I'd consider capping the number of results displayed to a reasonable number. (1,000 seems as good as any.) This is autocomplete; no one is really looking through all the results – they'll continue typing until the resultset is manageable for a human.

josh3736
  • 139,160
  • 33
  • 216
  • 263
  • Okay, this is great! I had started thinking about something like this but I wasn't sure if the time to delete the contents of a
      and recreate them (in one move) was faster than just altering a CSS rule (the actual document.styleSheets....) that already exists. You're pretty sure that it is?
    – Nick Aug 12 '12 at 20:12
  • Yep. See the demo I just added. – josh3736 Aug 12 '12 at 20:13
2

I know this is question is old BUT i'm not satisfied with any of the answers. Currently i'm working on a Youtube project that uses jQuery Selectable list which has around 120.000 items. These lists can be filtered by text and than show the corresponding items. The only acceptable way to hide all not matching elements was to hide the ul element first than hide the li elements and show the list(ul) element again.

aLx13
  • 701
  • 5
  • 16
1

You can select all <li>s directly, then filter them: $("li").filter(function(){...}).hide() (see here)

(sorry, I previously posted wrong)

Lord Spectre
  • 761
  • 1
  • 6
  • 21
  • 1
    @MilanJaric: The OP's original method, if I understood right, is to go through all the `
  • ` tags with `each()`, and apply `hide()` to each and every one of them. What this answer suggests is to filter them all into a single JQuery collection, and then apply `hide()` to all of them. That way, `hide()` is only called **once**.
  • – Idan Arye Aug 12 '12 at 19:50