Lessons in Javascript Performance Optimisation: 90 seconds down to 3 seconds

Summary: Avoid $$(“.classname”) on large DOMs!!!

I’ve recently been optimising the guts out of a JS webapp I wrote, which was making IE crawl to a halt. I discovered this after introducing a stress-inducing data set. (Using Rails’ fixtures makes light work of this; since the fixtures are Ruby templates just like the web templates, it’s easy to introduce a loop to create lots of data.)

With a rather large data set (100+ items, each several fields), IE would take about 90 seconds to churn through the initial script before the user could do anything. Firefox would run the same thing in about 8 seconds, still too long for a web page, but incredibly about ten times as fast as IE. I’m wanting to avoid pagination at this stage, so first priority was to tweak performance and see if we can keep everything on the same page.

After some sophisticated profiling ((new Date()).getTime():D), the main culprit was revealed to be prototype’s $$. It’s a fantastic function, but if you try to grab all elements belonging to a certain class, and the DOM is really big, $$(“.cssClassName”) can be slow. REALLY SLOW in IE. Remedy:

  • Removed trivial usages of $$() – e.g. in one case, the script was using it as a simple shorthand for a couple of DOM elements, and it was easy enough to hardcode the array. i.e. $$(".instruction") becomes [$(“initialInstruction”), $(“finalInstruction”)]. The former notation is cuter, but unfortunately impractical on a large web page.
  • Introduced the unofficial selector addon. Seems to have improved performance in more complex queries, i.e. $(“#knownId .message”), but doesn’t seem to have affected performance of $$(“.classname”).
  • Finally, I bit the bullet and scrapped $$(“.classname”) altogether. It’s more work, but the script now maintains the list of elements manually. Whenever an element is added or removed, the array must be adjusted. Furthermore, even the initialisation avoids using $$(), thanks to some server-side generated JS that explicitly declares the initial list of elements belonging to the class (i.e. the list that would normally be returned by $$()). To do this, the following function is called from onload(), generated with RHTML.

javascript

  1. function findAllItems() {
  2. <% js_array = @items.map { |item| "document.getElementById('item#{item.id}'),"}.join
  3.       js_array = js_array[0..-2] if @items.length>0 # Drop extra comma at end -%>
  4.       return [<%= js_array %>];
  5. }

The last step explicitly identifies all items in the class, removing the need to discover them by traversing the DOM. I wasn’t really sure how much time it would save – after all, you still have to look the elements up in the DOM and assign them to the array. But when I tried it, the savings were supreme – on IE, from around 45 seconds to about 2 seconds.

I have also incorporated Dean Edwards’ superb onload replacement to get the ball rolling before images are loaded. It’s a neat trick and takes 5 minutes to refactor it in.

Ajax Programming Patterns – Podcast 4 of 4: Performance Optimisation Patterns

The fourth and final podcast in this series of Ajax Programming Patterns. As always, the patterns are online at AjaxPatterns.org and covered in the book too, now available at Amazon. This 33-minute podcast covers seven patterns of Performance Optimisation:

(Note that the last two are recent additions to the wiki and just stubs at this stage.)

Okay, here endeth the series. I will soon be starting up a new series on the next group of patterns (Part 5 in the book): Functionality and Usability Patterns. There will be a change in the format, one I hope you’ll enjoy!

Pseudo-Threading: Multithreading in the Browser

You know AjaxPatterns? It’s a wiki about Ajax. Anyway, it’s now fully open for editing, but I’ll post more about that later. Right now, this post covers a particular pattern that’s been sitting in eXtreme Stub mode for some time, and has now got a little flesh to it.

Pseudo-Multithreading (mmmm…just rolls off the tongue) is a Performance Optimisation pattern to make input smoother. Now that the wiki’s open, you could even contribute some info if you’ve used it.

(The links below don’t work as it’s a straight HTML copy.)


Forces

  • Ajax Apps are single-threaded. Browsers don’t allow scripts to multithread, and nor does Javascript have any built-in support for it.
  • Most Ajax Apps accept user input.
  • Some Ajax Apps require complex processing in the browser. If the thread of execution is busy performing such processing, users won’t be able to perform input.

Solution

Using Scheduling, a processing function is called once in a while, incrementally processes a bit more of the problem, before yielding. Instead of solving the entire problem at once and returning, the function maintains a “blackboard” object and continuously works on it until the problem has been solved. This “blackboard” object is optional and may be something that forms the eventual solution, or just a copy of the original problem and an indication of what to do next.

For example, imagine you’re implementing a Portlet, a real estate advertisement providing a mortgage rate calculation. The calculation requires you to run a simulation, calculating the value at the end of each day for a year – a loop of 365 calculations. If you do it all at once, the user won’t be able to do anything during that long period. So instead, you break it into chunks of 10 days at a time. At the end of the first chunk, the blackboard indicates the situation after 10 days. At the end of the second chunk, the blackboard indicates the situation after 20 days. At some point, it will reach its target, 365 days, at which point it will probably call a callback function or just handle the result directly, e.g. paint the result on the user-interface.

It’s convenient to handle this in an object-oriented fashion, where the blackboard data and the processing function belong to the same object. The object is basically a Strategy or Command object (Gamma et al) – it has a “run()” method that does a little bit of processing, and sets values as attributes of itself. In the above example, we have a Calculator object with a run() function that performs the 10-day simulation. calculator.run() begins by reading the daysSoFar attribute, calculator.daysSoFar and the valueSoFar value, calculator.valueSoFar. It then simulates the next 10 days, and updates those attributes. Calculator might also include additional attributes too, such as: the number of days to simulate per run() invocation; the completion condition (which could be a function itself, or just a value such as number of days); a callback function to call upon completion.

Alternatives

Thin Client

“Thin Client” Ajax Apps delegate any complex processing to the server. Probably more network and server overhead, but less CPU cycles devoted to processing and more to user input.

Related Patterns

(TODO Patterns like Live Search, Portlet, and Live Form should point here.)

Real-World Examples

NumSum

The Ajax spreadsheet NumSum, continuously recalculates cell values, using Pseudo-Multithreading.