DOM optimizations with jQuery

I really do enjoy optimizing applications. Up until this project most of my experience was in MySQL and PHP optimizations so optimizing a client side application written in JavaScript is a new challenge for me. The product I am blogging about contains a custom work order entry screen which utilizes the JQuery library extensively. It does heavy DOM manipulations and event bindings to speed up the entry process for the user.  We began noticing some slowness on large work orders with over 50 lines. No big deal though until you hit 200 and then it becomes very noticeable.

On these large record sets the browser is spending 42.8 seconds (I’ll use numbers from this outlier throughout this blog entry) modifying the DOM. After optimization this was cut to 4.9 seconds. In development I didn’t realize I’d be working with such large sets. Nor did it occur to me that accessing the DOM could be this slow. Here’s how I fixed it.

“Bottlenecks occur in surprising places, so don’t try to second guess and put in a speed hack until you have proven that’s where the bottleneck is.”

Rob Pike, Software Engineer Google

Tools

I used Chrome’s profiler a lot. It’s not as user friendly as it could be, but it gets the job done. I also used data from jsperf and various discussions on stackoverflow.

Accessing an elements offsetWidth attribute is slow, very slow

We are using the wonderful Chosen library for jQuery. Unfortunately our application was spending lots of time in it, 33.5 seconds of complete DOM blockage. No work is being done for a half minute as  a user sits waiting for this thing to chug away. In it’s defense, the developer probably never envisioned someone using Chosen on the page over 400 times.

The chrome profiler pointed me towards AbstractChosen.prototype.container_width(). It appears line 436 causes “browser reflows”. I haven’t educated myself on exactly what these are beyond they are slow. The solution is to set a width when you are instantiating chosen on a drop down so it does not have to access offsetWidth. This had a huge impact on performance:

     
Before Self Before Total Function After Self After Total
1230 ms 33525 ms Chosen.results_build:717 17 ms 802 ms
7131 ms 17492 ms SelectParser.add_option:49 123 ms 426 ms
6721 ms 24953 ms SelectParser.select_to_array:104 129 ms 549 ms

Obviously this turned out to be a very simple fix.

jQuery.children() is faster than jQuery.find()

I went through the code and replaced a lot of jQuery find calls to children. In this case we were able to improve performance by 70% (2280ms to 711ms) on find() operations. I searched through the profiler after this to find any mentions of children to see if this was offset and only found a few mentions totaling up to about 100ms. So this was definitely a win, but I feel may have been a borderline micro-optimization with the amount of code I had to change. JsPerf has some conflicting reports on this. One test showing jQuery.children() as the clear performance winner and another showing jQuery.find() as the winner.

“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.”

Donald Knuth, Computer Science Professor Stanford

jQuery.is() method is slow

We were only using this once, albeit in a loop, and 391 ms was being spent on it. I determined this wasn’t even needed so I just removed the condition.

Native for loop is faster than jQuery.each

Our application was spending a lot of time using jQuery.each. We know that a for loop is much faster than jQuery.each. So the decision was to remove them in areas where the application was looping a lot. Roughly 1 – 2 seconds were saved here.

Full Results

Before     After  
Self Total Function Self Total
10ms 42874 ms Cos.view_callback 10ms 4991 ms
10ms 42874 ms Cos.addRow 10ms 3198 ms
16 ms 41123 ms x.extend.each 35 ms 41123 ms
1230 ms 33525 ms Chosen.results_build:717 17 ms 802 ms
7131 ms 17492 ms SelectParser.add_option:49 123 ms 426 ms
6721 ms 24953 ms SelectParser.select_to_array:104 129 ms 549 ms
4072 ms 4072 ms get text 110 ms 110 ms
3858 ms 3858 ms get innerHTML 125 ms 125 ms
0 ms 391 ms x.fn.extend.is 0 ms 1 ms
2897 ms 2897 ms get offsetWidth 0 ms 0 ms

Last word

This project was definitely an eye opener as client side optimization had always been an after thought to me. My focus has always been on server-side code, particularly database access. For this project I decided to return JSON data from the server and render it completely with JavaScript. It’s probably not something I’d do again without the use of a framework that’s built to do this fast like Angular or just rendering sever-side and doing some minor on-page JavaScript.

I also wish I had tackled Chosen first. It was the last thing I looked at because I didn’t think it would be a simple fix. I figured I’d be rewriting portions of the library which I really didn’t want to do. Instead I spent maybe 10 minutes on research and 10 minutes on code changes. Those 20 minutes ended up resulting in massive improvements. For everything else many hours were spent changing and testing code. I had estimated 2 days on this originally and hit my estimate dead on, but really this should have been a super quick fix.

“The First Rule of Program Optimization: Don’t do it. The Second Rule of Program Optimization (for experts only!): Don’t do it yet.”

Michael A. Jackson, Computer Scientist