Fastest way to hide thousands of <li> elements?
Asked Answered
M

8

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.

Metzler answered 12/8, 2012 at 19:42 Comment(16)
can you add in question what is the condition, you may just use css if it is not that complexMessere
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.Ginn
@Ginn The problem probably stems from calling $ thousands of times.Becalmed
Why would you need to hide() all the .hidden elements? Shouldn't that be already defined in your stylesheet?Hydrosome
@jfriend00, the .hide() causes a page reflow thousands of times. Doing it once it surely faster.Metzler
@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.Metzler
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...Evvie
@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?Metzler
@Nick: Yes.Freeloader
@Becalmed - 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.Ginn
@Metzler - $('.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.Ginn
@Metzler - 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.Ginn
@Ginn 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.Metzler
@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.Ginn
@Ginn 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: jsfiddle.net/GekPY No reference to any CSS properties and it redraws the page every for loop.Metzler
@Metzler - 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: jsfiddle.net/jfriend00/fVPz7.Ginn
F
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.

Freeloader answered 12/8, 2012 at 20:5 Comment(2)
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 <ul> 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?Metzler
Yep. See the demo I just added.Freeloader
F
3

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.

Foxing answered 14/9, 2015 at 9:43 Comment(0)
S
1

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

(sorry, I previously posted wrong)

Silvanasilvano answered 12/8, 2012 at 19:44 Comment(1)
@MilanJaric: The OP's original method, if I understood right, is to go through all the <li> 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.Cosh
H
1

You can use the jQuery contains() selector to find all items in a list with particular text, and then just hide those, like this:

HTML:

 <ul id="myList">
      <li>this</li>
      <li>that</li>
 <ul>​

jQuery

var term = 'this';    
$('li:contains("' + term + '")').hide();​
Helmholtz answered 12/8, 2012 at 19:55 Comment(0)
N
1

You could use a more unique technique that uses technically no JavaScript to do the actual hiding, by putting a copy of the data in an attribute, and using a CSS attribute selector.

For example, if the term is secret, and you put a copy of the data in a data-term attribute, you can use the following CSS:

li[data-term*="secret"] {
    display: none;
}

To do this dynamically you would have to add a style to the head in javascript:

function hideTerm(term) {
    css = 'li[data-term*="'+term+'"]{display:none;}'
    style = $('<style type="text/css">').text(css)
    $('head').append(style);
}

If you were to do this you would want to be sure to clean up the style tags as you stop using them.

This would probably be the fastest, as CSS selection is very quick in modern browsers. It would be hard to benchmark so I can't say for sure though.

Necrolatry answered 12/8, 2012 at 20:26 Comment(0)
F
0

How about:

<style>
    .hidden{ display: none; }
</style>

That way you don't have to do the extra query using $('.hidden').hide() ?

Faithless answered 12/8, 2012 at 19:46 Comment(1)
"display : none" cause dom reflow. and it will be slow. Better option is "visibility : hidden"Perjure
M
0

Instead of redefining the Stylesheets rules, you can directly define 'hide' class property to "display:none;" before hand and in your page, you can just apply the class you defined after verifying the condition through javascript, like below.

$("li").each(function(){if(condition){$(this).addClass('hide');}});

and later, if you want to show those li's again, you can just remove the class like below

$("li").each(function(){if(condition){$(this).removeClass('hide');}});
Mixture answered 12/8, 2012 at 19:53 Comment(0)
F
0

Use the insertRule on a newly created stylesheet:

// create and append a new stylesheet
const style = document.createElement('style');
document.head.appendChild(style);
// get the sheet part of the stylesheet
const styleSheet = style.sheet;
styleSheet.insertRule('li.hidden{display:none;}');

I've not benchmarked this but a assume the internal browser code probably is a bit faster than a javascript loop.

I realize you mentioned document.Stylesheets but this is a more isolated approach.

Facile answered 18/12, 2023 at 15:46 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.