Towards a Cleaner, Leaner Website

I found some time in the past few weeks to work on my website, and am excited to finally get this up.

After reading a lot of resources about page bloat such as this and that, I wanted to reduce the heavy bloat in my current website. I decided its easier to just build a new site than to remove old code.

Here are a few of the goals I wanted to achieve:

  • Less bloat. I had no defined metric for this, but I tried to add as little js/css as possible when developing the site.
  • Works without Javascript. e.g. the sidenav uses an html checkbox for state, and css transitions.
  • Performance. If I am able to achieve the above goals, then the Time To First Interactive should be minimal. After all, it should be a relatively simple static site. Unsuprisingly I scored 100 in the performance section of the Lighthouse test.
  • Mobile first design. It’s 2018, and according to StatCounter, more than half of web traffic comes from mobile devices. I think mobile-first is a sensible approach.
  • Email obfuscation (see /contact page). I don’t anticipate my site getting much traffic for this to be a problem, but it’s nice to know that I am hiding my email from bot crawlers and potential spam.

Why did I migrate to Hugo? Were there any issues with using Jekyll for managing posts? No. I felt it was worth trying out a new static site generator. When I looked into updating my site back in early 2017, Hugo was, and still is, one of the more stable, widely adopted tools I researched. If I started today in late 2018, I would probably try Gatsby or Next.js (although I personally believe less popular compile-time frameworks such as svelte/sapper and stencil, along with basic web component wrappers like LitElement, are the future).

Asset pipeline

Hugo doesn’t have a built-in asset pipeline, other than copying over files from static folder to the final public site folder. And so I hacked together some scripts to create an asset workflow.

Initially I wanted to leverage my work experience and use good ol’ Webpack to handle all my bundling needs. However I wanted to explore outputting es6 code in the final bundle, and decided to use Rollup and friends to handle bundling and minifying js.

For CSS, PostCSS does the job nicely for bundling and minifying css. While I thought about including css as part of the rollup dependency graph ( by import styles from 'main.css' ), the css is simple enough to not warrant being part of rollup. Plus, it means I don’t need to run rollup during development mode.

Why did I not reach out to other tools in the golang space? Simply because there is a large community around these js-based asset tools. There’s a high probability that any problems I face can be answered by the community.

Dev Server

It’s frustrating to always build assets when we make a code change. Hence, it’s typical to have some sort of dev server to efficiently bundle assets while developing (See webpack-serve and rollup-plugin-serve). These plugins are efficient because they only send the code change deltas to the dev server via websockets instead of traditionally copying over the whole file.

But since es6 modules are supported in many browsers nowadays, it seemed unecessary to setup a transpiling toolchain just for development. Running hugo server is good enough as its quite fast in detecting and updating changes in the frontend theme code. However I still wanted to create a build toolchain for production builds to handle minifying, cache-busting, etc.

Building Production Assets

This was a little tricky because I wanted these requirements:

  1. continue using hugo server for development mode.
  2. bundle for production:
    1. minify JS & CSS bundle, and also html.
    2. add hash to JS & CSS bundle filenames for cache-busting.
    3. automatically add hashed output bundles to html head.
    4. gzip JS, CSS, and HTML. (gzip handled by Google Cloud’s gsutil cp -z html,css,js command)
    5. cross platform npm scripts.

I intended to mostly have JS and CSS, so no need to worry about image assets. A combination of plugins, along with some Hugo template functions and scripts, allowed me to accomplish the above tasks:

package.json

...
  "scripts": {
    "clean": "shx rm -rf static/bundle.*.{js,css} public/",
    "build:css": "postcss themes/simple/static/src/main.css -o static/bundle.css",
    "build:js": "rollup -c",
    "build:html": "cross-env BUILD_ENV='production' hugo && html-minifier -c htmlminifier.config.json --input-dir public --file-ext html --output-dir public",
    "build": "npm run clean && npm run build:css && npm run build:js && npm run build:html && shx rm -rf public/src",
    "start": "npm run clean && hugo serve"
  },
...

postcss.config.js

module.exports = {
  plugins: {
    "postcss-import": {},
    "postcss-preset-env": {
      browsers: "last 2 versions"
    },
    "postcss-clean": {
      level: 2
    },
    "postcss-hash": {
      trim: 8,
      manifest: 'data/css.json'
    }
  }
};

rollup.config.js

import hash from 'rollup-plugin-hash';
import uglify from 'rollup-plugin-uglify';

export default {
  input: "themes/simple/static/src/main.js",
  output: {
    file: "bundle.js",
    format: "es"
  },
  plugins: [
    uglify({
      compress: {
        inline: 1
      }
    }),
     hash({ 
       dest: 'static/bundle.[hash:8].js',
       manifest: 'data/js.json',
       replace: true
		})
  ]
};

layouts/partials/head.html

...
  {{ if eq "production" (getenv "BUILD_ENV") }}
    <!-- prd mode -->
    <link rel="stylesheet" href="{{ .Site.BaseURL }}{{ index .Site.Data.css "bundle.css" }}">
    <!-- remove 'static/' from `data/js.json` manifest file generated by `rollup` -->
    <script type="module" src="{{ .Site.BaseURL }}{{ substr (index .Site.Data.js "bundle.js") 7 }}"></script>
  {{else}}
    <!-- dev mode -->
    <link rel="stylesheet" href="{{ .Site.BaseURL }}src/main.css">
    <script type="module" src="{{ .Site.BaseURL }}src/main.js"></script>
  {{ end }}
...

Improving Performance

As mentioned above, performance is important to me for production build. Here’s a summary of the techniques used:

  1. Bundle CSS and JS.
  2. Minify, and gzip HTML, CSS, and JS.
  3. Defer script execution with “defer” script attribute.

Traditionally, we do (1) because browsers limit the number of http/1.1 connections for each resource. Bundling is a widely used technique to serve more code in a single http connection (See also: image spriting and subdomains). But nowadays with http/2, we multiplex all the resource requests for a domain into a single tcp connection. Thus, it could be faster to just request each resource seperately instead of going through the trouble of bundling them together. Still the byte savings from compressing bundles could lead to faster response times, which might justify the added toolchain complexity.

For now bundling seems to make sense since the resource size dramatically went down, but I need to do some more testing on this. Also, I did not have time to research and setup https on Google Cloud Storage.

Deploying

Deploying to Google Cloud Storage

Future TODOs

  • Go through website checklist.
  • Performance optimizations:

    • Inline critical styles/scripts above the fold.
    • Prefetch resources on mouse hover. (i.e., hovering on link to new page, fetch resources for that page before clicking)
    • See if we can fit the initial http request under 14kB. Will have to dig deeper and see what potential issues will arise (maybe “flash of unstyled content”, etc).
    • Write script to automate some of these optimizations.
  • Automate deploying the static files to Google Cloud Storage, AWS S3, or any other platform.

  • If start having more client-side code, then maybe think about: