Ever since I got my copy of Steve Souders’ Even Faster Web Sites I’ve became obsessed with speed. During my day job I’m constantly looking for things that can be improved to make the user experience smoother, specially for first-time visitors. I’m fairly happy with what we’ve achieved in the last year, though there are always things to be improved.
We’ve started using YUI in Landscape around March of 2009, by rewriting all of our ExtJS-based scripts (which weren’t that many) to use YUI. Since our application requires HTTPS due to potentially sensitive information, we had to self-host YUI, to avoid mixed-content warnings. Initially we had little experience with it, so we went with the easiest option available: loading the YUI seed file and letting everything else be loaded on-demand by the YUI Loader, without a combo handler (more on that later).
Soon enough we noticed that it wasn’t such a great idea. First-time access was terrible, with load times over 30 seconds in some cases, due to the combination of HTTPS connection overhead, no static resource caching and large number of requests to fetch all the resources. It was worse than that even, because most browsers either don’t cache HTTPS content by default, or only cache it on memory, unless you set the proper caching headers. To give you an idea of what it looked like, here’s what a trivial page using the ‘overlay’ module looks like in terms of loading.
No combo loading at all
It was pretty clear that this kind of performance would not be acceptable, so I started looking for options to reduce the number of requests. It was around that time that I heard about Steve Souders work on web performance, through a long time friend from the Plone community that happened to be working at Google and shared a link to Souders’ blog post on ‘@import’.
Turns out that the YUI Loader supports using a combo handler for reducing the number of requests, when loading modules on demand. What this ‘combo handler’ does is basically take a GET request with a bunch of filenames as parameters and return a single response with the respective files concatenated. Here’s an example using the combo server from YUI’s CDN.
Combo loading with a single 'Y.use()'
The problem was that we couldn’t use YUI’s combo server on the CDN because it doesn’t support HTTPS, which was requirement for us.
In November of 2009, at UDS-Lucid in Dallas, we had our first YUI-only cross-team sprint, totalling about 20 people from Launchpad, Landscape, ISD and Ubuntu One. The main goals were to spread knowledge on YUI and improve support for IE on our shared codebase (lazr-js), but I managed to sneak in two things for the Landscape team: implementing a Python-based combo loader (served by Twisted, but it’s just a plain WSGI app) and extracting YUI module metadata, so that we could make it easier to load extension modules on-demand, the code for which ended up as part of the lazr-js project.
At this point however we realized that there’s a significant issue with using the combo loader on an application as diverse as Launchpad or Landscape: depending on how you group your ‘Y.use()’ calls, the combo URL generated by the YUI Loader can vary significantly, which means that re-use from the browser cache is less than optimal. Here’s an example of loading both the ‘anim-color’ module and the ‘overlay’ module within a page. Compare it to the previous example and notice how the parameters passed to the combo are completely different, since the second request (to load the ‘overlay’ module) only requests modules which haven’t been loaded by the first request (to load the ‘anim-color’ module).
Combo loader can be a caching problem
In a hurry to get the situation solved ASAP, and taking a clue from our fellow developers on the Launchpad team, we went with a simpler alternative: manually combining all of YUI, including the seed file, and loading all of that in a ‘script’ tag in the document ‘head’. It turns out that the situation improved quite dramatically, but there was still more to be done. Although the number of requests was much smaller, the total page size was actually larger, since we were loading a lot of stuff that wasn’t even being used. Worse, since we were putting that ‘script’ tag in the ‘head’, it was blocking the whole page from loading until the script was finished downloading, which I after following Souders’ blog for a while I realized wasn’t such a great idea either. Here’s an example of what that looked like.
Combined modules in the head
At this point we had a significant codebase of YUI-based modules, and so did the Launchpad team, but we had gone from solely relying on the YUI Loader to not using it at all (by just loading all the code in one big honking file).
Once we got the module metadata extraction figured out though, I resumed working on improving the first-view load time, by reducing the number of modules manually combined to the bare minimum shared by all of our pages. Also, by having a single file with the common modules being loaded on all the pages, we can be sure that it is highly cacheable. And then we let the YUI Loader kick in and load the missing modules on less-used pages, which was a really nice improvement in page weight and reducing the time to the ‘onload’ event.
At this point Souders started talking about asynchronous loading of scripts and the ‘defer’ attribute, and I got my copy of Even Faster Web Sites. I started pondering if there was something that could be done to defer the loading of the combined modules while still letting the YUI Loader do it’s job.
The problem lies in the fact that YUI asynchronously loads modules that are not on the page yet, calling a callback function (the last argument to ‘Y.use()’) once all the scripts containing the required modules have finished loading. If all the modules are already on the page, it just calls the callback right away. So by just tacking a ‘defer’ attribute on the ‘script’ tag for the combined modules there would be a race condition between the loader checking if the modules were loaded and the script with the combined modules being loaded.
Then, it struck me. If the last argument to ‘Y.use()’ is just a callback function, could we queue those functions to be called at a later time and load the combined modules ourselves, before letting the YUI loader proceed? After a little back and forth with Dav Glass and some pseudocode exchange, I got an implementation of this idea(which I’m calling the ‘Prefetch YUI Loader Hack‘). And thus we managed to move the loading of all the combined modules from a ‘script’ in the ‘head’ to use asynchronous script loading, shortening even further our time to the ‘onload’ event. Here’s an example of what it looks like.
Prefetch with a single request
But wait, there’s more! Souders then started talking about parallel loading, tickling my speed bug again. After thinking for a while about the problem, enlightenment came again: when an YUI module is loaded, it is not executed right away, but added to an internal registry of modules. What that means is that regardless of what other modules it depends on, they can all be loaded independently from each other! That means you can break up a manually combined file into N smaller files and have them be loaded in up to N parallel connections (where N varies by browser and by browser version) and when they are all done you can let the YUI Loader kick in. Here’s an example with two parallel downloads plus an extra module not prefetched.
Prefetch, with parallel download
So that’s where things stand now. There are still optimizations that can be done in Landscape, like using the combo loader to reduce the number of requests on less common pages, or even using fixed URLs to the combo loader with the prefetch hack, to simplify the build process. We also need to start using Caridy’s Event Binder module, since now the pages load so fast that the users start clicking around before the event handlers are in place (ha!).
I am also pushing to get those kinds of tips documented and passed around Canonical, through a project codenamed Dare2BFast, which has the goal of coming up with a set of Web Performance Guidelines that all Canonical web sites should follow, both on the frontend and backend. Stay tuned!