Performance tuning for mobiles

After two and half days I am piecing together the frayed ends of my sanity, I feel like I have been mind raped by 100 horny baboons. This is the pleasant feeling you will be left with after you attempt to performance tune your website for mobiles.

Initial enlightenment

Previously, I had modified this website with an attempt at a responsive design so that it would scale nicely to the various, and I mean various (there are a freck'n lot of them) screen sizes on mobile an tablet devices. I was feeling pretty good about myself, thinking "oh, now I can handle mobile viewers happily", and then I watched Google I/O 2013 - Instant Mobile Websites: Techniques and Best Practices.

The lessons of the YouTube movie were many, but simply put, 3G sucks rhino testes. 100-200 ms RTT (round trip time) per request! It gets worse if the file is over 14k. So 200 ms per request, at least, and you haven't even received a single bit of data yet. Wow, how technology improves ... we are back to dial up speeds. Add onto that, redirects and possible HTTPS connections and it could be quicker having it chiseled into stone and send via parcel post.

Well, the plebs do like their mobile tardtech devices, and we better keep the plebs happy else their be an uprising, so best investigate my own site.

Initial shock of knowing nothing

After that, I popped over to  WebPagetest, whacked one of my "thought" URLs into it, picked an iPhone test device somewhere in the US (Shaped 3G (1.6 Mbps/768 Kbps, 300 ms RTT) and clicked the Start Test button and waited. Now I thought I knew what I was doing with web development, minimised, single file Javascript and CSS, GZipped text responses, optimised images, clean HTML ... the results taught me the same lesson: You know nothing John Snow.

Initial, non-cached response was 14 seconds, something around 20 requests and according to the filmstrip view of the load you can see on WebPagetest, most of that time the iPhone had a white page with no content. Second load was a bit better, but something around 7 seconds, but still what the Germans would call verdammt Scheiße (please correct my German here).

The attack

Lesson 1 of the enlightenment: Avoid landing page redirects

Yep, not a problem for me, but I have seen it all over the shop. Go to a web address and suddenly it turns into a URL prefixed with an "m". Fail! You just cost the user 200 milliseconds. The correct answer is to build a responsive CSS site that will adapt to the device, not redirect. Already had that in the bag, so crisis adverted there.

Lesson 2 of the enlightenment: Minimize server processing time

The extreme answer to this is to render out your HTML as static files and serve these. Well, I am not taking too much load on this host and the amount of effort to do this would be a lot for the payoff. In the WebPagetests 1.5 to 2.5 seconds is spent on the host doing all that PHP and MySQL stuff, I think I can live with that. I did explore the SilverStripe framework (what this is being served with) to see if there were any caching points, but the SilverStripe kids had already cached this up fairly well. Overall did nothing here except it was a good audit of my queries and such.

Lesson 3 and 4 of the enlightenment: Eliminate render blocking resources and Prioritize visible content

I stuck these together as this was my problem. 20-ish requests, it was the number of connections rather than the size. The talk shows the rendering pipeline of the browser, the big lesson here was that regardless of whether the HTML DOM has been parsed, if an external CSS file is being loaded, it will show nothing. The poor user sits and watches a white screen until after the external CSS file has been loaded. So while part of the load of the CSS file will be in parallel to the HTML, it adds more time.

Delay, delay, delay!

For mobile requests, I inlined a small amount of styles to get a basic look. Nothing more then some sizes and very simple typography. I changed all CSS and Javascript requests to be asynchronously loaded. Of course, I keep the normal single CSS file in the head for non mobile devices.

The first trick is to detect that the user is using a mobile device so you can tailor the behaviour. I already do this for other reasons, but here is the basic PHP snippet which matches against the user agent that I use (I am sure there are better ways to do this):

$device = "Screen";
$system = "Unknown";
if (preg_match('/ipod/i', $agent)) {
    $system = 'iPod';
    $device = "Handheld";
} elseif (preg_match('/iphone/i', $agent)) {
    $system = 'iPhone';
    $device = "Handheld";
} elseif (preg_match('/ipad/i', $agent)) {
    $system = 'iPad';
    $device = "Handheld";
} elseif (preg_match('/andriod/i', $agent)) {
    $device = "Handheld";
    $system = "Android";
}

if ($device == "Handheld") {
    // Do the mobile behaviour
} else {
    // Phew ... non slow device, do the default behaviour
}

Now for a little bootstrapping asychronous Javascript to load the CSS and Javascript. The below example is pretty much what this site is doing. I need the Javascript to be loaded in order, hence they are loaded one after the other. The CSS is just injected in without delay:

/**
 * @namespace
 */
window.Meerware = window.Meerware || {};
(function(window, document) {
    /**
     * @param {String|Array}
     *            requirements
     * @param {Function}
     *            callback
     */
    var require = function(requirements, callback) {
        if (!callback) {
            callback = function() {
            };
        }
        if (typeof (requirements) == "string") {
            requirements = [ requirements ];
        }

        var current = document.getElementsByTagName('script')[0];

        /**
         * @param source is the source location to load.
         * @param callback is the optional callback.
         */
        var script = function(source, callback) {
            var element = document.createElement('script');
            element.type = 'text/javascript';
            element.async = true;
            element.src = source;
            element.onload = element.onreadstatechange = function() {
                var ready = this.readyState;
                if (ready && (ready != 'complete' && ready != 'loaded')) {
                    return;
                }
                // Call again
                window.setTimeout(callback, 1);
            };
            current.parentNode.insertBefore(element, current);
            return element;
        };
        
        /**
         * Stylesheets are just loaded, we don't care about a callback being called
         * before the thing is ready, just bloody do it!
         * @param source is the css source.
         * @param callback is the optional callback method.
         */
        var stylesheet = function(source, callback) {
            var element = document.createElement('link');
            element.type = 'text/css';
            element.rel = 'stylesheet';
            element.href = source;
            document.getElementsByTagName("head")[0].appendChild(element);
            window.setTimeout(callback, 1);
            return element;
        };

        /**
         * Loads an individual script and calls the next one in a timeout.
         */
        var load = null;
        load = function() {
            // Shift off the queue
            var next = requirements.shift();
            if (!next) {
                callback();
                return;
            }
            if (next.lastIndexOf(".js") > 0) {
                script(next, load);
            } else {
                stylesheet(next, load);
            }
            
        };

        // Start it up
        load();
    };
    // Expose our require method via the namespace
    window.Meerware.require = require;
})(window, document);

I took this code and whacked it into the online Closure Compiler and pasted this to be the first item in the body.

The next trick is to actually call this with the files, putting the CSS first. Notice that this includes Google web font CSS as well:

// Wrapped in a tiny timeout to give the renderer some extra time
window.setTimeout(function() {
    Meeware.require([
        "//fonts.googleapis.com/css?family=Muli:300normal,300italic,400normal,400italic",
        "styles.css",
        "//cdnjs.cloudflare.com/ajax/libs/mootools/1.4.5/mootools-core-full-nocompat-yc.min.js",
        "javascript.js"]);
}, 1);

In fact, I use the same asynchronous call to initialise Google Analytics and even for normal non handheld loads I use this to get the Javascript. Re-use, recycle!

Fallback, retreat!

But Matt, didn't you rant on previously about not being reliant on Javascript for the look and layout of your website? Well yes I did Dr Chewbacca T Howlett, thanks for reminding me. As a fallback, for the case of scripts being turned off for whatever reason, we loose the asynchronous opportunity but you shouldn't penalise the user, a very simple noscript tag (which you are now allowed in HTML5) in the head wrapping the CSS external call will do it:

<noscript>
    <link href='styles.css' type='text/css' rel='stylesheet'/>
</noscript>

Kill those requests!

We now have asynchronous loading but we still have too many requests. All those pesky little images and icons. Where it made sense, I replaced them with a  Base64 encoded image. Ok, you loose the easy of editing the images and it can bloat your CSS, you also can loose caching of them, but if they are small it makes sense. There is an excellent encoder here, one of those crazy Germans again ... works damn well. Do also keep in mind, there is a limit in IE8 in terms of the size of 32k, but if you are converting a 32k image into a Base64 encoded string, maybe you are doing it wrong. Also this will not work in IE7 or earlier ... but then, who cares about IE7 users anymore? I certainly don't, less than 1% of usage ... no care here.

The final result

The final result is much better, 7.5 seconds fully loaded on a mobile device for the first load. Text starts appearing in 2-3 seconds if not faster. There is a caveat that as the main CSS and webfonts are loaded there is a change to the main layout, but I have engaged the user much quicker. A repeat load of the page is 2.5-3 seconds. Comparing that with other blog sites, I was seeing 20+ seconds in some cases, so I feel like I achieved something and even the performance of the non mobile device is much quicker. Job done!