Published on by Steve Mathias
In the case study below, our client informed us that mobile users, particularly those using Chrome on an Android device, reported dissatisfaction in the site's performance in an infinite scroll context. It would stutter, lock up, and then catch up shortly resulting in the content jumping around. It was disorienting, it was confusing, and it had a serious potential to reduce traffic, both short and long term. Through research, profiling, and careful optimization, we were able to preserve the entire user experience while improving the performance of that experience on all devices, most particularly lower-powered devices such as phones and tablets.
Site profile of Foodgawker before optimization
The questions we had to answer were why it took so long, and what we could do to minimize both the amount of time that it spends processing our behaviors, as well as the amount of time our behaviors took away from the browser at once (resulting in visible locks and stutters in the browser interaction.)
Ultimately, we came to the following conclusions:
- Improve our jQuery selection and processing
- Allow multiple event handlers to thread and run asynchronously if other code does not rely on them.
- Reduce the CSS complexity of the site and improve CSS performance.
Improving jQuery selection and processing
The first quick gain, without changing existing behaviors, is to look at the jQuery selectors used in the site. It is important to remember that the jQuery selectors operate very similarly to the browser's CSS selectors, with many of the same considerations for performance. For example, the right-most selector should be the most specific, when possible, in a selector string that contains multiple selectors. For example, you might change the selector “div.wrapper-class h1” to “div h1.wrapper-title” for a small performance gain. jQuery will also handle selectors quickest when using IDs, with class-based selectors next in performance.
$("input:hidden"), it is often a very simple performance gain to do
It's also important to remember that such queries take time. If you're going to repeatedly refer to a certain selection (such as “body”), store it in a local variable and query it only once. This will prevent re-running the query multiple times, often offering an extremely large gain over time on operations the site performs multiple times.
Creating asynchronous event handlers
This can be a subtle trick that can really make a site more performant, even if the code itself does not change. When event handlers are processed in jQuery, the handlers are run synchronously (in real time) and sequentially (one after another). Adding a handler will put it at the end of the list, but this order should not be counted on, so depedency shouldn't be built between one handler and another without triggering an event in the first handler. However, this means that, if you have three handlers that each take 500 milliseconds to run, the browser will be “locked up” for 1.5 seconds in this alone. That is an extremely noticeable time to a user, particularly when this is supposed to be a silent interaction in a fluid state such as infinite scroll.
How, then, do you do the same work, but cut it down so that the browser only locks up for 500ms or so at a time? Through asynchronous handling. Your event handler can encapsulate functionality inside its own closure, and run that through a setTimeout() call. In doing so, you allow control to go back to the parent jQuery event, and eventually the browser, much faster by delaying action immediately. This allows it to get through all 3 handlers in something closer to 1ms right now, do a browser update, and then run them. Further, if the browser supports multiple threads, it can even now process these more or less simultaneously instead of synchronously, greatly improving the visual performance of the browser. The more often we get control to the browser, and the longer it stays there, the more fluid the interaction will be.
Reducing and optimizing CSS performance
CSS can be simplified most easily by ensuring that rules are as lightweight as possible, and that any deprecated rules that are no longer applicable to the site are removed. You can go through further optimization in the same method as mentioned above for jQuery, leveraging the right-to-left specificity within your CSS rules. This could mean that it may even be valuable, in limited cases, to split your CSS into multiple files and ensure that you're not loading rules in a scope that you do not intend to use.
Through these changes, we were able to return control to the browser much more often, and much more quickly, than we had prior to these updates. There are still occasional issues in reflow and redraw as the DOM gets heavier, in particular in ad code. Further optimization and changes could improve this as well, though there sometimes are constraints that are outside our control (such as optimization of third party ad scripts). More improvement could also come from removing features and simplifying the overall process, potentially, with client cooperation. However, these updates already offer substantial improvement with no feature loss, resulting in a happier client and happier users on the client's site.