Friday, October 3, 2008

Huge performance boost during iterations

I've improved my code to the point during this latest round of optimizations, that I can get Safari 3.1 to load 4,356 divs (each with a few attached events, and 5 attributes, including styling) - to load in 657 milliseconds (.15 ms per item)

The status of my system at the time was a 50% memory allocation (of 1 GB), iTunes running, a text editor running, and the one browser. No heavy background services were running (polling).

Give it a try below and help me test! Please send me your results... :) To be fair, try closing down your apps and let ONE browser test at a time, with ONE tab. Don't let other processes get in the way. This will be the cleanest number.

If you get different results than me, or if the documentFragment results are slower, then don't freak out. Although its unlikely that this would happen, remember there is an entire world of variables to consider that make this kind of testing tough to streamline. Thats why I am exposing this and want your help.

FYI: The mouseover event does exist in both tests, and is being allocated, even if the addOption test doesn't mouseover with any visual changes. These samples are both in their own iframes, to ensure no preference.

  • You can perform two kinds of tests with both these boxes.
  1. The first is to put a large number into the box and run each. Then jot the results and refresh. (The first test simulates how a user would feel when a page loads and they have to wait for contents to render.)
  2. The second test is one where you load the boxes with a reasonable number, (say 100), then keep loading over and over again without refreshing. (The second test simulates an interface where a user is tasked with "adding elements" throughout their session. How does the box respond?)

Safari is by far the fastest rendering engine (all rendering engines are not the same! Google Chrome has added elements to their rendering engine, alongside webkit, that make it just slightly slower than Safari in the current 0.2. At least in my tests).

With this test I am creating divs that emulate a select box set of options. The top is my current performance, and the bottom is simply using addOption() to append native options to a select box in the DOM (I've tested this a dozen times in all, 10 times each browser (120 tests), ask me for the complete results and I will send.):

504 items:

Firefox 3 (me) : 198 ms (.4 ms per item avg)
Firefox 3 (native addOption) : 358 ms (.7 ms per item avg)

Opera 9.5 (me) : 177 ms (.4 ms per div)
Opera 9.5 (native addOption) : 136 ms (.3 ms per div avg)

Safari 3.1 (me) : 78 ms (.2 ms per item)
Safari 3.1 (native addOption) : 50 ms (.1 ms per item avg)

Chrome .2 (me) : 203 ms (.4 ms per item)
Chrome .2 (native addOption) : 416 ms (.8 ms per item avg)

Ie7 (me) : 359 ms (.7 ms per item)
Ie7 (native addOption) : 382 ms (.8 ms per item avg)

The key to all of this is when and where. If you write your methods correctly, you can massage where code is invoked, and when to push your performance over the edge.

1. When it comes to large scale iterations, don't use the toolkit DOM methods to create and insert the bulk of your objects.
  • I know, it's crazy. But hear me out. There are pain points somewhere in your application where things slow down when using a toolkit. What I am saying here is that during the page load, or when returning an ajax response of 150 divs (e.g.) - don't use the toolkit to wrap these up just to get special features.
  • You can use the toolkit to iterate, but during that iteration, separate out the methods to help minimize the dirtiness. Inside those methods, get dirty and write plain old javascript for a huge performance boost* (see below to see more of what I mean here).
  • Meaning, don't use the append, create or addevent (bind) methods (see more detail below), but use everything else (createElement, onmouseover = function, setAttribute). You can certainly use these for most of your operations, sure, and that's the point of a toolkit. But not for these heavy iterations!
  • Create your own internal "shadow" array in memory of these objects with its base styling and base events, then, when the DOM has rendered, and for the rest of your application, wrap jQuery (or new Elements $) around your items and proceed as normal to style and add effects - or wait for just the right time to invoke these styles and events.
2. Use a documentFragment when creating elements.
  • documentFragment is your #1 friend. Instead of appending to the DOM at the end of every iteration, append to a documentFragment Element, then lastly, append the documentFragment element to the DOM.
  • This works 3x faster because it is all kept in memory - not in the DOM, where rendering is slow. It is added to the DOM later and without any effort on your part, the documentFragment melts away.
  • This has been around for years and supported by all browsers.
3. Use very short custom attribute names.
  • Except for id, className and title, make your custom attribute names be 2 characters or less. This will shave many milliseconds off your rendering time.
4. Bind Events within Events
  • Did you know that if you really plan, that you only have to bind one event to an object? Yes, the mouseover event! Inside that event, bind the rest of your events conditionally. Why pay that price over and over again during iterations?
5. Add classes and styling carefully. (IE can choke)
  • Here is where things get hairy in IE, but if you must make ie run faster, then do this: Set the className (or "class" in non-ie browsers) by hand using setAttribute. Faster than any toolkits "addclass". Don't even use the item.className = class. Just use setAttribute.
6. For ie, use innerText, not createTextNode
  • to inject text. I saved 30-80 ms per item doing this
7. Use Native events like onmouseover = function()
  • ...not toolkit addEvents or bind. Except for the little trick I mentioned above, don't bind or addEvent() during heavy iterations.
*What I mean by dirty: I mean writing plain old javascript. And by "inside these methods" I assume that you are using some kind of OOP-like style and have separated your methods too keep them small and to make sure they do one thing well. That being the case, I assume that during an iteration, you have one method for just attaching events. Its in here that you "get dirty" and write plain old javascript.

  • Aside from the two types of tests, you need to test both types of boxes. One uses documentFragment technique, and one uses addOption by comparison.
  • Note: if you get the "browser / script is running too error" let me know. The documentFragment test works differently in different browsers, but the addOption test can't handle large numbers! Another testament to using documentFragment.
  • Note: if you want to see how I did the tests, please grab the js file in the source. If you want an explanation of what I did, please comment and I will give much more detail (probably not necessary for this).
If you want to see examples I will definitely share!


Joshua said...

My results were as follows, for 1000 iterations:

addOption results: (269) ms
documentFragment results: (145) ms
FF 3.0.3

addOption results: (672) ms
documentFragment results: (328) ms

addOption results: (438) ms
documentFragment results: (148) ms

Windows XP x64 Pro

zorglub76 said...

My browsers:
FF 2.0.17
addOption results: (221) ms
documentFragment results: (278) ms

FF 3.0.3
addOption results: (185) ms
documentFragment results: (142) ms

FF 3.1.b1 (nightly)
addOption results: (167) ms
documentFragment results: (125) ms

Safari 3.1.2
addOption results: (55) ms
documentFragment results: (71) ms

Opera 9.5.2
addOption results: (137) ms
documentFragment results: (141) ms

addOption results: (186) ms
documentFragment results: (117) ms

addOption results: (600) documentFragment results: (327) ms

The Ui Guy said...

Thanks guys! Interesting such varied results. I am going to put both tests in two different iframes to ensure no ordering preference in my tests. I continue to wonder how much the current CPU cycle and memory allocation affect performance.