Dynamically loading assets with pjax and require.js

With the new story pages for MPR News we’re using a combination of techniques to make the site load very fast.

43 http requests, 780KB, 2.6s load, notbad.gif

One of those techniques to make our site fast been to combine the CSS and javascript for the site into a single file. This keeps our HTTP request count low, which keeps pages fast. This works fine for just having one or just a few types of pages, but what happens when we have more than story pages, and have pages that require wildly different css and javascript? E.g., if a visitor comes to the home page, but never sees a story page, they don’t need the assets for a story page loaded. Our initial build system for the site would bundle all that up, meaning our hypothetical visitor would get that CSS and javascript they didn’t need.

We’ve solved this by changing our build system to build different files for each major section of the site, plus base files that are shared between all sections. So, visiting a story page, you’ll receive this:

  • base.min.css (7.6k gzipped), <link> in html
  • story.min.css (2.8k gzipped), <link> in html
  • init.js (71k gzipped) <script src=…> in html
  • story.js  (1.6k gzipped) loaded dynamically via require.js

This is twice as many files and HTTP requests as had been previously necessary, which isn’t great, but the tradeoff is worth while considering how slow loading all sections of a site could be. We haven’t built all sections yet, so we don’t know precisely, but we can imagine that it’d suck. Even if the file size stayed reasonable, the proliferation of event bindings and unused CSS selectors would weigh heavily on page performance.

Enter PJAX

Our setup is complicated somewhat by our use of PJAX (pushState + ajax). Since we don’t do a full page reload, something needs to load up the new CSS and javascript for new sections as a visitor navigates through our site. Enter router.js:

Router.js executes at initial pageload to load any javascript dependencies for the present page path (line 8). On future PJAX events, it looks at the new route and tries to load CSS and JS dependencies for the new route. We’re using pjax:start (line 15) for CSS loading so that we can get the CSS before new markup is injected and avoid any FOBUC. We wait until PJAX is done (pjax:complete, line 9) because new javascript probably has selectors that need to run against markup that needs to be present. We also care less about the JS firing instantaneously. I wouldn’t claim that router.js is robust or sophisticated, but it works for us so far.

We’re using the very helpful require-css plugin for require.js to dynamically load CSS assets after initial pageload. On initial pageload, we define the CSS for both the base and the given route in the HTML. Again, this avoids FOBUC and waiting on javascript.

Building with grunt

We use grunt as our front-end build thingamajig. Here are the relevant parts of our Gruntfile.js:

Grunt-contrib-requirejs is a very direct mapping for the require.js optomizer (r.js), which is indispensable. R.js analyzes our require.js setup, traces the dependencies, and builds discrete minified .js modules that map to our routes in router.js. Every time we add a new route that has new js dependencies, we need to update our Gruntfile.js and router.js.

A word of warning: the paths, dirs, and baseDirs setup in require.js / r.js can be tricky and took me some trial and error to get right for the build setup that we wanted.

Assets & expire headers

We’ve also improved our asset versioning. In development mode, our site loads javascript out of the /js/ path. In production, after we’ve run grunt deploy, javascript is served out of /js-built/{site version}/, where grunt-contrib-require builds it to. Our other css, image, and font assets are served out of /a/{site version}/ in production mode. Previously, we had been versioning individual files, but we are now versioning our asset folders directly. This means we can set very long expires headers on all our assets, yet when we make a change to the site, the assets will update reliably.

Here’s is the relevant mod_rewrite chunk of our apache config:

Since we use a CDN, Akamai, we want our CDN to update frequently from our origin servers, but to advertise far-future expires to all clients. The way you do this with Akamai is to set the expires for your CDN as your normal expires header, and then serve an Akamai specific header that tells their network what to rewrite your expires to. Here’s the config:

Comments are closed.