Servitude: CSS and JavaScript Injection Sugar

Tuesday, December 27, 2011 - Jerry Sievert

Multiple requests suck. Let me rephrase that: multiple requests really suck. The more requests you make, the more connections that are made, the more data that is transfered, the longer it takes to for your application to become responsive. Servitude helps you reduce the number of requests made for CSS and JavaScript down to one and injects it into the DOM for you in the order you request -- faster requests mean more responsiveness; more responsiveness means a better application.

Size Matters

You should be familiar with using gzip and deflate to lower the file size of the download, but there are still some parts of the request that you can not or should not be mucking with: specifically the headers. Not all headers are created equal but there's still a lot of data being transfered. Let's take a look at some headers from the internet wild:

    jerry@Super-Light:~$ curl --silent -D /tmp/headers http://www.yahoo.com/ >/dev/null
    jerry@Super-Light:~$ wc -c /tmp/headers
        1956 /tmp/headers
    jerry@Super-Light:~$ curl --silent -D /tmp/headers http://www.bing.com/ >/dev/null
    jerry@Super-Light:~$ wc -c /tmp/headers
        1303 /tmp/headers
    jerry@Super-Light:~$ curl --silent -D /tmp/headers http://www.google.com/ >/dev/null
    jerry@Super-Light:~$ wc -c /tmp/headers
         764 /tmp/headers

Wow. That's a lot of data, and that's per request. In fact, that's more data in the request header than most of the microjs.com libraries/frameworks. Multiply that by each request and it can really start to add up.

Typically, reduction occurs by combining as many files as possible into a single file and requesting that file. That approach is good, but still has a couple of flaws:

  • either every page must share the same resources, or multiple permutations exist
  • caching can become difficult
  • still requires separate CSS and JavaScript requests

Injection

What happens if we instead combine the CSS and JavaScript together and inject each into the DOM in the proper order? Enter Servitude, an easy way to do just that. Let's take a look at how Servitude works in the browser.

Injecting CSS

    // create a style element to inject
    var styleElem = document.createElement("style");
    
    // label it (really nice for debugging)
    styleElem.setAttribute("data-injected-css", servitude.css[i].index);
    styleElem.setAttribute("type", "text/css");
    
    // figure out where to inject it, either after the last style or after the first script
    // (don't worry, we needed a script tag to inject this stuff anyways, there will be one)
    styles = document.getElementsByTagName("style");
    domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0];
    domTarget.parentNode.appendChild(styleElem);
    
    // we're injected, let's provide some content (and a fallback for IE)
    // this is usually done in a loop, so we have a servitude array
    if (styleElem.styleSheet) {
        styleElem.styleSheet.cssText = servitude.css[i].contents;
    } else {
        styleElem.appendChild(document.createTextNode(servitude.css[i].contents));
    }

That's it, CSS has been injected.

Injecting JavaScript

    // create a script element to inject
    var jsElem = document.createElement("script");
    
    // label it (again, really nice for debugging)
    jsElem.setAttribute("data-injected-javascript", servitude.js[i].index);
    jsElem.setAttribute("type", "text/javascript");
    
    // figure out where to inject it
    domTarget = document.getElementsByTagName("script")[0];
    domTarget.parentNode.appendChild(jsElem);
    
    // we're injected, provide the content
    jsElem.text = servitude.js[i].contents;

And from there, the JavaScript has been injected and we're good to go -- but what difference does it make? Quite a lot, as it turns out.

example without using Servitude

example using Servitude

These examples are only slightly contrived: they are real-life examples taken from Celci.us, but are being served up locally from a very fast SSD. Add in a little bit of distance and the difference starts to get a lot worse.

Using Servitude - Server Side

Servitude is simple to use if you are using Bricks.js. Simply install Servitude with NPM:

    $ npm install servitude

Setting up a route is pretty straightforward at that point. You need to define the path that Servitude responds to, and include a Regular Expression grouping (inside parentheses):

    var bricks = require('bricks');
    var servitude = require('servitude');
    
    var appserver = new bricks.appserver();
    
    appserver.addRoute("/servitude/(.+)", servitude, { basedir: "./htdocs" } );
    
    var server = appserver.createServer();
    server.listen(3000);

Servitude will respond to any request directed to /servitude and return all documents that are comma separated for injection. Optionally, you can include a cache and uglify option, to speed things up and to minify on the way out.

Using Servitude - Client Side

A simple script tag is all you need to use Servitude on the client side:

    <script src="/servitude/css/main.css,css/page.css,js/jquery.js,js/page.js"></script>

This will inject the CSS and JavaScript in the order requested.

Using Servitude is extremely easy, and helps out responsiveness quite a bit. Fork it, use it, improve it.