Build­ing My Site Part III: Fine Tuning

Publisher: TJ Fogarty

Modified: 2018-03-13

After mak­ing some deci­sions and imple­ment­ing them, it’s now time to tidy a few things up and improve on per­for­mance. I’m going to talk about the ways in which I’ve improved the load­ing of fonts, CSS and JavaScript.

Fonts #

I pre­vi­ous­ly read an arti­cle called 23 Min­utes of Work for Bet­ter Font Load­ing and it’s a bril­liant piece of work out­lin­ing the ways in which font load­ing can be improved. I didn’t fol­low every step, but the two I imple­ment­ed real­ly made a difference:

First, pre­load­ing the web fonts by putting these tags into the <head> of my site:

<link rel="preload" href="/fonts/raleway.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/playfair.woff2" as="font" type="font/woff2" crossorigin>

Sec­ond­ly, adding those fonts to my ser­vice work­er using the SW Pre­cache Web­pack Plu­g­in.

CSS #

I opt­ed to use the Tail­wind CSS Frame­work to style my site. I found it a great way to throw a bunch of class­es on my ele­ments to rapid­ly style them, and abstract them out into their own class­es once I was hap­py with them.

If you have a look around, you might notice that there isn’t a whole lot of style here. That may change in the future, but it also remind­ed me of Har­ry Roberts’ web­site where he inlined his styles with­in the <head> of his site rather than using a <link> tag.

Fair enough, but there’s a lot of CSS being pulled in with Tail­wind that wouldn’t make sense to include. I’d only be need­less­ly increas­ing the weight of my page. Thank­ful­ly the author of this frame­work has a solu­tion! PurgeC­SS to the rescue:

// webpack.mix.js file
const mix = require('laravel-mix')
const tailwindcss = require('tailwindcss')
const glob = require('glob-all')
const PurgecssPlugin = require('purgecss-webpack-plugin')

// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
  static extract(content) {
    return content.match(/[A-z0-9-:\/]+/g)
  }
}

if (mix.inProduction()) {
  mix.webpackConfig(
      plugins: [
        new PurgecssPlugin({
          // Specify the locations of any files you want to scan for class names.
          paths: glob.sync([
            path.join(__dirname, 'templates/**/*.twig'),
            path.join(__dirname, 'web/assets/**/*.js')
          ]),
          extractors: [
            {
              extractor: TailwindExtractor,

              // Specify the file extensions to include when scanning for
              // class names.
              extensions: ['html', 'js', 'php', 'twig']
            }
          ]
        })
      ]
  )
}

This scans through tem­plate and JavaScript files, and strips out any class­es that are not being used.

In layouts/default.twig I inline it using the Craft Mix plugin:

{{ mix('/assets/css/app.css', true, true) | raw }}

JavaScript #

There’s some JavaScript on my site that isn’t required to be loaded with every sin­gle page, those being highlight.js and Algo­lia Search. Not every page requires syn­tax high­light­ing, and not every­one will click the search icon, so I need­ed a way to only load them when it was nec­es­sary. I did this with Dynam­ic Imports and some tweak­ing of my webpack.mix.js file.

When I was first using it, the chunks that were cre­at­ed were either dropped into the wrong direc­to­ry, or the path they were loaded from were incor­rect. Here’s what I added to my con­fig­u­ra­tion to cor­rect it:

mix.webpackConfig({
  output: {
    path: path.resolve(__dirname, 'web'),
    publicPath: '/',
    chunkFilename: 'assets/js/chunks/[name].js'
  }
})

Let’s look at syn­tax high­light­ing first. I want­ed to check if a page had code on it, and load the library in if it did.

if (document.querySelector('pre')) {
    try {
      let hljs = await System.import(
        /* webpackChunkName: "hljs" */ 'highlight.js'
      )
      hljs.initHighlightingOnLoad()
    } catch (e) {
      console.log('Error loading highlight.js', e)
    }
}

Using the com­ment /* webpackChunkName: "hljs" */ I could spec­i­fy the name of the gen­er­at­ed file. Oth­er­wise you’d end up with files called 0.js, 1.js etc…

Next up is the search. I’ve stripped out most of the inter­ac­tion code here, and left in the load­ing of the required library:

import { env } from './utils'

export const Search = {
  trigger: document.querySelectorAll('.js-search'),
  index: null,

  init() {
    this.handleTriggerClick = this.handleTriggerClick.bind(this)

    this.trigger.forEach(trigger => {
      trigger.addEventListener('click', this.handleTriggerClick)
    })
  },

  handleTriggerClick(e) {
    e.preventDefault()
    this.loadSearchClient()
  },

  async loadSearchClient() {
    try {
      let algoliasearch = await System.import(
        /* webpackChunkName: "search" */ 'algoliasearch/lite'
      )

      let client = algoliasearch(
        'applicationId',
        'apiKey'
      )
      this.index = client.initIndex(
        env() === 'development' ? 'dev_posts' : 'prod_posts'
      )
    } catch (e) {
      console.log('Error loading search client', e)
    }
  }
}

I have an index for my local devel­op­ment ver­sion, and the pro­duc­tion ver­sion. I have a func­tion called env which helps me with which envi­ron­ment I’m in:

export const env = () => {
  return process && process.env && process.env.NODE_ENV
    ? process.env.NODE_ENV
    : null
}

Is it fin­ished? #

After all this, I have a deploy­ment script that runs npm run production which mini­fies my assets, gen­er­ates a ser­vice work­er, and strips out unused CSS class­es. Cou­pled with the font load­ing tech­niques I have a zip­py lit­tle site.

There’s more I can do, how­ev­er. Maybe there are small­er libraries out there that I can swap in, or some opti­mi­sa­tions I can make to my arti­sanal, hand-rolled code. I could add the JavaScript to the ser­vice work­er, though I have cache-inval­i­da­tion trust issues that I need to work through first.

Until then, you can see the source code for this site as it stands today.