Friday, April 24, 2015

I Sped Up An Already Spartan Website

Recently I was reading about the Mobile Friendly Search Initiative by Google. On a whim, I decided to subject my domain to the test. To my utter horror, it reported that my simple design was not very mobile friendly.

Well, it wasn't exactly old news to me. I have used my own web site every now and then to look up some of the lists that I have online, from my reading list to my shopping list. I've long discovered that the ``auto-reflow'' option that was present in my phone's web browser was breaking my web site, and I never really knew why.

Now that I had a third party tool to demonstrate objectively that yes, my web site was not very mobile friendly, I started to find ways to fix it.

The first and most important thing was to add a new meta tag to my pages so that the page will scale according to the device's actual width as opposed to the raw pixel count. This is important because of higher pixel densities of the mobile devices compared to the usual run-of-the-mill generic monitor, but expect this to change over time as those 4k (and 8k) displays become more mainstream. For the sake of completeness, the tag looks like this:
<meta name="viewport"
 content="width=device-width, initial-scale=1"/>
However, that got me thinking even more, especially after using the Page Speed Insights tool from Google. They highlighted some interesting things to think about, namely:
  1. Removing blocking Javascript/CSS;
  2. Minification of HTML/CSS/Javascript;
  3. Enabling browser caching;
  4. Enabling server compression.
I tried to do the first one by adding the async="async" keyword to each of my script tags, but it failed the XHTML 1.0 DTD validation, so I had to abandon it. That was a big no-no, so I had to abandon that tip that was suggested.

Before doing the second one, I made a note of the total file size of all the files that are involved (XHTML, CSS and Javascript only)---it was 205640 bytes. To actually do the second one, I wrote a customised script in Python that made use of csscompressor and slimit together with the built-in HTMLParser object to walk through the XHTML tree and perform the minification. I didn't replace the files with the minified forms---I am still editing them by hand with Vim---but created an output directory to dump all these files in. This step alone reduced the file size to 185602 bytes, around 90% of the original set of files. Specifically, prettyprint.js shrunk from 3876 bytes to 1299 bytes, or around 34% of the original file size. This is significant considering that all the pages that I ever author end up pulling that file. This is definitely a drastic saving in the long run even without turning on browser caching.

The next step is to turn on browser caching. It took me a while, but creating a .htaccess on the root directory of the public-facing web site with the following lines did the trick:
<ifModule mod_expires.c>
  ExpiresActive On
  ExpiresDefault "access plus 1 seconds"
  ExpiresByType text/html "access plus 1 seconds"
  ExpiresByType image/gif "access plus 2592000 seconds"
  ExpiresByType image/jpeg "access plus 2592000 seconds"
  ExpiresByType image/png "access plus 2592000 seconds"
  ExpiresByType text/css "access plus 604800 seconds"
  ExpiresByType text/javascript
    "access plus 216000 seconds"
  ExpiresByType application/x-javascript
    "access plus 216000 seconds"
</ifModule>

<ifModule mod_headers.c>
  <filesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|swf)$">
    Header set Cache-Control "max-age=2592000, public"
  </filesMatch>
  <filesMatch "\.(css)$">
    Header set Cache-Control "max-age=604800, public"
  </filesMatch>
  <filesMatch "\.(js)$">
    Header set Cache-Control "max-age=216000, private"
  </filesMatch>
  <filesMatch "\.(xml|txt)$">
    Header set Cache-Control
      "max-age=216000, public, must-revalidate"
  </filesMatch>
  <filesMatch "\.(html|htm)$">
    Header set Cache-Control
      "max-age=300, private, must-revalidate"
  </filesMatch>
</ifModule>
Caching made a nice difference in that the scripts and CSS (like prettyprint.js) need to be loaded only once and everything is nice and dandy.

The last step of turning on compression on the payload turned out to be the hardest to pull off. At first, I was trying to use the deflate or gzip mods for Apache 2.2 (the server that NearlyFreeSpeech.net uses), but after not noticing any changes by spying on the response headers from HTTP in the client browser, I changed tack by using mod_rewrite instead. This meant that I had to append the following lines to .htaccess:
<ifModule mod_rewrite.c>
  Header add Vary accept-encoding
  RewriteEngine on
  RewriteCond %{HTTP:accept-encoding} gzip
  RewriteCond %{REQUEST_FILENAME} !\.gz$
  RewriteCond %{REQUEST_FILENAME}.gz -f
  RewriteRule (.*\.(js|css|html)) $1.gz [L]
</ifModule>

AddEncoding x-gzip .gz

<FilesMatch .*\.html.gz>
  ForceType text/html
</FilesMatch>

<FilesMatch .*\.css.gz>
  ForceType text/css
</FilesMatch>

<FilesMatch .*\.js.gz>
  ForceType text/javascript
</FilesMatch>
What this does is that it tries to look for a pre-compressed version of the requested file (seen as %{REQUEST_FILENAME}.gz) and returns that while adjusting the headers to inform the client that a GZIP stream is incoming. It's a form of trickery but it got the job done well. The last part of it was to adjust the minification script I wrote to generate the pre-compressed files as well. This new set of files generated an overhead of 66252 bytes, which meant that now, the total size of files hosted is 251854 bytes, or roughly 122% of the size of my original set of files. Specifically, prettyprint.js is now only 565 bytes, a cool 15% the size of the original file. The original files are needed in the event that the client browser doesn't support a gzip stream.

Even with the overhead though, I find the new set up way more responsive and totally worth it. It does mean that I need to do a little bit more work before I upload an updated file, but it does mean that now, the already spartan web site can be accessed much faster in more places.

Alright, enough of this babble talk. Till next time.

No comments: