[转]Now you see me… show/hide performance

http://www.learningjquery.com/2010/05/now-you-see-me-showhide-performance

I just got back from the jQuery conference in San Francisco. Wow, what an event. In addition to some incredible talks, I had the opportunity to speak with Rey Bango, Johnathon Sharp, and, of course, John Resig. Any conference where you get to talk to some of the most influential people in jQuery is a win in my book. The "High Performance JQuery" presentation especially caught my attention when the speaker, Robert Duffy, said that .hide() and .show() were slower than changing the css directly. Not having occasion to ask him why, I benchmarked the various ways to hide DOM elements and looked into the jQuery source to find out what is going on.

The HTML I tested against was a page of 100 div tags with a class and some content, I cached the selector $('div') to use with each method to exclude the time needed to find all the div elements on the page from the test. I used jQuery 1.4.2 for the testing, but keep in mind that the algorithms behind the method calls can change dramatically from version to version. What is true for 1.4.2 is not necessarily true for other versions of the library.

The methods I tested were .toggle().show() & .hide(),.css({'display':'none'}) & .css({'display':'block'}), and.addClass('hide') & .removeClass('hide'). I also tested modifying an attribute of a<style> element.

.show() & .hide()

These were, in fact, comparatively slow methods of hiding DOM elements across all browsers. One of the main reasons is that .hide() has to save the notion of what the display attribute was before, so that .show() can restore it. It does this using the .data() jQuery method, storing that information on the DOM element. In order to do so, .hide() loops through every element twice: once to save the current display value, and then once to update the display style to none. According to a comment in the source, this prevents the browser from reflowing with every loop. The .hide() method also checks to see if you pass in a parameter to animate the hiding with an effect. Even passing in a 0 dramatically slows down the performance. Performance was slowest on the first call to .hide(); subsequent calls were faster.

Browser      hide/show
FF3.6 -         29ms / 10ms 
Safari 4.05 -   6ms / 1ms
Opera 10.10 -   9ms / 1ms
Chrome 5.0.3 -  5ms / 1ms
IE 6.0  -       31ms / 16ms 
IE 7.0  -       15ms / 16ms 

.toggle()

This was, by far, the slowest method of hiding all of the div elements. It iterates through every element returned by the selector, checks to see if the element is currently visible, and then calls.hide() on visible elements one at a time and .show() on hidden ones one at a time. It also has to check to see if you are passing in a boolean to force everything to .show() or .hide() and check to see if you are passing in functions to toggle instead of toggling visibility. There seems to be some opportunity for optimization of this function, as one could select all of the hidden elements of the selector and call .show() on them all at once and then select the remaining elements in the selector and call .hide() on them at the same time. If you are so inclined, I encourage you to check out the source and see if you can eke out any performance gains.

Browser      hide/show
FF3.6 -         80ms / 59ms 
Safari 4.05 -   24ms / 30ms 
Opera 10.10 -   67ms / 201ms
Chrome 5.0.3 -  55ms / 20ms 
IE 6.0  -       296ms / 78ms 
IE 7.0  -       328ms / 47ms 

.addClass() & .removeClass()

These are pretty snappy methods of hiding/showing elements of the DOM, twice as fast as.show() & .hide() in Firefox and three times as fast in Safari. The differences in IE 6, IE7, Chrome, and Opera are negligible. It's also worth noting that with 100 DOM nodes, we're talking a total difference of 18ms in Firefox and 4ms in Safari. The speed difference will only be relevant for very large selections. Adding and removing a class requires a bit more management on your part, since you have to create the class that has a display of none and then have to keep track of CSS priority to make sure your elements get hidden. The way jQuery adds and removes a class is through string manipulation, so I imagine that as the number of classes on an element grows, this method will get slower, but that is untested speculation on my part.

Browser      hide/show
FF3.6   -       11ms / 11ms 
Safari 4.05 -   2ms / 2ms
Opera 10.10 -   6ms / 3ms
Chrome 5.0.3 -  3ms / 1ms
IE 6.0  -       47ms / 32ms
IE 7.0  -       15ms / 16ms

.css({'display':'none'}) & .css({'display':'block'});

These methods were very snappy. They showed an improvement over .addClass() and.removeClass() in Opera and IE 6/7 and about the same in other browsers. They work great if you know the current display style of all the elements you are changing, or at least have not changed the display style inline. If you have changed the display style inline, then you will need to make sure you set the correct value when you make the element visible again. If you are just using the elements' default display value or set the display value in the css, then you can just remove the style like so, .css({'display':''}), and it will revert to whatever value it has in the css or by default. As a library, jQuery can't assume that the display element wasn't set inline, so it has to manually keep track of it. That is the main slowness you can avoid since you know you won't be setting the display inline.

Browser      hide/show
FF3.6   -       14ms / 12ms
Safari 4.05 -   2ms / 1ms
Opera 10.10 -   2ms / 2ms
Chrome 5.0.3 -  2ms / 1ms
IE 6.0  -       16ms / 16ms
IE 7.0  -       0ms / 0ms  // The usual caveat about inaccuracy of IE clocks applies.

Disabling stylesheets

For fun, I thought, "What if instead of manipulating every DOM node and changing things, we just futz with the stylesheet?" Could there be speed improvements there? I mean, the methods benchmarked above are plenty fast for everyday use, but what if I had 10,000 nodes on a page I wanted to show and hide? It would be slow just selecting them all. But, if I could manipulate the stylesheet, I could avoid the entire overhead. Let me just tell you that way is fraught with peril.

There are, of course, cross browser issues when manipulating stylesheets, since jQuery doesn't abstract them away for you. First, I tried to see if I could append a style tag with the css class as a string using jQuery, but got inconsistent results across browsers. Then I tried creating the stylesheet node and class using JavaScript, but there were different APIs and it ended up being too slow to justify. So finally, forgoing an attempt to do this in a programmatic way, I ended up just writing a style tag with a class in the head of the document. It's far too slow to create the stylesheet programmatically, but if it's already there then it is trivial to give it an ID and use its disabled attribute.

HTML:
  1. <style id="special_hide">.special_hide { display: none; }</style>
  2. <!--  ...  -->
  3. <div class="special_hide">Special hide DIV</div>

Then in javascript…

JavaScript:
  1. $('#special_hide').attr('disabled, 'true');

and BAM, you just displayed all of your elements with a class of “special_hide”. To hide them all again, just do…

JavaScript:
  1. $('#special_hide').attr('disabled', 'false');

and they are now all hidden. The total javascript processing time was 0-1ms across all browsers. The only javascript you are doing is changing an attribute. Of course, there is still the time the browser takes to reflow and repaint the page, but you've virtually eliminated all the javascript processing time. If you call any of the other methods, .toggle().hide(), or .css(), this method will stop working on those elements because they set the css style inline, which has higher precedence than other css. To make this method work again, simply do a .css(‘display’, ‘’) to remove the inline style. This method also requires the most work on your part, because you have to define the class and give the class to all of the elements on the page you want to show/hide at the same time, but if you are dealing with extremely large sets, this might just be worth it.

To recap, here is a list of methods to change the display of elements in order from fastest to slowest:

  1. Enabling/Disabling a stylesheet
  2. .css('display', ''), .css('display', 'none')
  3. .addClass(), .removeClass()
  4. .show(), .hide()
  5. .toggle()

Also note that for the majority of use cases, all of these methods are plenty fast to use. When you start having to manipulate large jQuery collections, .show() and .hide() might become too slow in IE, and you might need to bump up to .addClass() or .removeClass(). Enabling/disabling of stylesheets would only be necessary in the most extreme cases, but if things are hiding to slowly for you, you might want to give it a try. 

posted @ 2010-05-11 16:00  Kimura  Views(225)  Comments(0Edit  收藏  举报