Performance notes, May 2017
“Added @dracos’s traintimes site to my phone home screen – it is faster than any of my apps” – @jukesie, Twitter
On Twitter last week, Bruce Lawson asked people to write up their performance optimisations. I’ve had some bits of time to make some improvements to traintimes.org.uk, and so here is a short essay/notes (I don’t get much free time at present for various small-person-shaped reasons) on how this site is currently seven times quicker than the official site on a mobile:
(WebPageTest, Manchester UK, Chrome, emulated Motorola G gen 4, slow 3G)
Transferring goods
The server now runs nginx with HTTP/2
activated for those browsers that support it, which e.g. allows requests to be
multiplexed, reducing the need for things like spriting or JavaScript
concatenation. This was as straightforward as making sure I had a recent
enough version of nginx/OpenSSL (I use the packages from
DotDeb), and adding
http2
to the listen line.
(Okay, you also have to make sure the server is SSL, but I had already done
that for the service worker and geolocation; thanks to
Let’s Encrypt and StartSSL
before that).
All the JavaScript is minimized with the Closure Compiler, and the CSS is minimized with SCSS; nothing fancy going on there, as long as they’re scripts that can be called from the command line rather than needing some build tool framework. I use file modification times in the query string to enable long caching (you could also use a content hash), and all the static content is gzipped.
I’ll include a full breakdown at the end, but suffice it to say that Google’s Lighthouse tool, which audits web pages for performance, installability and more, and on which traintimes.org.uk scores 100/100, says:
Avoids enormous network payloads: Total size was 9 KB (target: 1,600 KB) — Lighthouse
Punctuality
The first time you visit a traintimes.org.uk page, the CSS is inlined in the HTML; slightly more bandwidth that first time, but it means there is no display blocking at all waiting for the CSS file. The CSS file is then loaded asynchronously with a slightly adapted loadCSS JavaScript, cached, and a cookie is set to the CSS filename, so that subsequent requests (until the CSS file changes) do not inline anything (assuming the CSS will be served from a cache, be that service worker, browser cache, proxy, whatever!). If you have a lot of CSS, you probably would not want to inline all of it, and might want to implement some form of “critical CSS” generator to minimize the amount inlined, loading the remainder asynchronously. Or at least split up your CSS in some manner to reduce the amount needed, whether it’s inlined or not. HTTP/2 offers server push which would theoretically provide a similar sort of thing, but nginx doesn’t yet support it, and even if it did you still need a first-use-cookie as Filament Group explains, plus keep the old behaviour for HTTP/1.1.
For JavaScript, I follow Jake’s advice and put script elements at the end of the HTML. This is what I have always done, so it is nice to know that doing nothing pays off in the long run.
With all the above, the site scores 100 in Google PageSpeed Insights, which is a very useful tool for this area of web development. (If you pick a results page, the mobile score can be a tiny bit less due to the time taken to get the results.)
Please do not leave baggage unattended
I was using jQuery on traintimes.org.uk. To give you an idea of the age of this site, it was jQuery 1.3.2. Actually, that doesn’t give you an idea, because jQuery 1.3.2 was released in 2009, this domain has been running since 2004, and my original live departure boards (with an image map) went live in December 2001, meaning parts of this code can drive a moped and get married this year.
But I digress. As the lovely jQuery file size page shows, jQuery 1.3.2 is 19.2KB in size, gzipped. Did I really need all that for the few bits and bobs I was using on the site?
Thanks to sites such as MDN and You might not need jQuery, and by dropping support for old IE (in much the same way as jQuery 2 has done anyway, though that now sits at 29KB, with 1.12 at 33KB), I was able to replace jQuery with only the few parts I was using, in vanilla JavaScript. This dropped the size of JavaScript on each page from well over 20KB to more like 2KB; okay, it would have been cached, but still. I also split out the code into separate files for each part of the site – front, result, live – because many people only go directly to one of those and don’t ever need e.g. the front page geolocation.
core.js is my “jQuery replacement”,
as it were. I kept a jQuery-like feel, with functions like
$.getJSON
, and by adding functions such as load
and
closest
to Element’s prototype. Perhaps I could have restructured
the HTML to reduce these, but this seemed easier :) I got a tiny domready from ded. I think in the end it is
quite cute and readable.
The hardest actual change was replacing all my jQuery
show('fast')
/hide('fast')
s with CSS transitions. I
could have just dropped the sliding up/down, but I wanted to keep it if
possible and in the end succeeded by adding a transition on max-height. This
slightly annoyingly only works if you have a fixed height for the block, rather
than ‘auto’, so there is a bit of JavaScript that works out the hidden height
of what is going to be displayed. Of course, it all displays fine if JavaScript
is not available, defaulting to visible – something I wish I could say was true of all sites.
Going off the rails
The site runs a service worker at guards-van.js, which is built upon others I have seen around, such as Jeremy Keith, Jake and others I have forgotten, sorry, blame sleep deprivation.
A service worker is some JavaScript that sits in the background of your site and can e.g. intercept requests to your site before they happen and do things with them. There are various helpful patterns out there such as the offline cookbook or the Service worker cookbook. My service worker does the straightforward things you see service workers doing around the place:
- It has a list of offline static assets – JavaScript, CSS, images, and an offline page – and downloads and stores them in a cache when installed;
- It turns
caches.match()
into a proper Promise, rathern than something that resolves with undefined upon no match; - For the front page, it uses the “cache and fetch” mechanism, returning the page from the cache if present, but always updating from the server at the same time;
- For any other HTML pages, it will fetch (we want up-to-date timetable responses!), then cache the response; if the request fails (e.g. we’re offline) it will see if the page is in the cache and display it if so, otherwise display an offline page;
- For non-HTML requests, it will look in the cache first, and only fetch if it can’t find it in there;
- The offline page will display a list of the cached pages you have stored (so you can hopefully get back to the times you’re after even when underground in a cutting sat outside New Street).
Service workers can do background syncing, push notifications, and more, but
you don’t have to use all the whizz bang stuff to do something useful. Also,
as they are progressive and only work in modern browsers, you can use new
JavaScript language features such as =>
arrow functions.
The buffet car
A mobile National Rail results page is 380KB; even with a primed cache it is up around 151KB. And if you look at the console when it’s loading, you will see why it takes so long to display anything in the video at the top – third party JavaScript blocking the display.
Here are my current transfer sizes:
- An entire results page from cold is currently around 8–9KB – that’s 2KB HTML, 1.9KB CSS, 2.3KB JavaScript, 1.8KB images, and 0.4KB manifest. After the service worker is installed, or even just with your browser cache, further pages are the HTML only. Nothing should stop the page from displaying as soon as possible.
- A live departure board is 7KB – slightly less JavaScript, fewer images.
- The front page is a massive 10.2KB (bigger HTML, slightly more JavaScript), but we can cache it and much of what it fetches is cached for the next page. Perhaps I should put the help/news on a different page…
“it’s so lightweight now, it’s quicker to use than actually looking at the announcement boards in a larger station.” – @gwire, Twitter
I hope that was of interest. In my day job, I also wrote a blog post on improving JavaScript performance on FixMyStreet; as we have Windows Lumia users there, I had to use AppCache for offline use, which was fun. Let me know if you have any questions, and do add your station or journeys to your home screen :)
— Matthew, @dracos