Optimizing a Grav-Installation

Aggressively chasing performance gains

When the term “Web 2.0” became popular after 2004, the focus was largely on design, usability, and new media. In the past few years, however, web development has focused a lot on more on speed and performance, as well as delivery of media across varied devices.

This is largely because websites have become much heavier since the 1990s, as websites have adopted new media and devices – including interactive elements, streaming video, audio, live data, and all else that we’re used to seeing on modern sites and apps these days. Images make up the bulk of this, and thus deserve special consideration. In this post I will outline some strategies for optimizing assets in a theme in Grav, and illustrate what results they bring. If you are only interested in the results, look here.

Preamble

I’ve been using the static-site generator Jekyll for my personal website since 2016, previously WordPress and shortly Grav, but in 2017 I wanted to return to Grav. The reason for this was mainly the Liquid templating-engine that Jekyll uses, which requires a lot of workarounds for fairly basic tasks. Before I started with Jekyll I considered the plethora of static generators available, and have continuously evaluated them since. At the time this was the simpler choice. It also has the largest community of developers and resources online.

When re-evaluating the available options in 2017, I wanted a framework rather than bootstrapping the build-process together myself, and the only real static-site generator alternative to Jekyll was Metalsmith in terms of maturity. However, the setup was unnecessarily complex yet limited, in that rather than simply handling Markdown-content and templating with a decent language like Twig, Nunjucks, or Jinja, a lot of declarations was necessary to accommodate the build-process I’ve used with Jekyll.

This build-process is based on using Gulp for optimizing assets and handles everything surrounding the site, thus relying on compiling content into templates. This was relatively easy with Jekyll, wherein the build is run with the simple execution of a command, and I wanted to maintain this simplicity going forward.

Automating Workflows

I run Gulp as a task-runner with Node. The build-process can be defined by these main steps:

  1. Build stylesheets using SCSS
  2. Build a static copy of the site using Wget
  3. Clean the CSS using UnCSS
  4. Minify the CSS
  5. Losslessly minify images
  6. Use Grav’s Asset Manager for pipelining or inlining JS

Thus, Grav alleviates most typical tasks ran by Gulp. Grav could of course also concatenate CSS-files, but Sass already handles that quite well.

Compiling Sass/SCSS

The stylesheets are structured with one principal file, which everything else compiles from; app.scss. Vendor-libraries are handled with NPM, and package.json includes:

"devDependencies": {
  "bootstrap": "^4.0.0-alpha.6",
  "compass-mixins": "^0.12.10",
  "flickrapi": "^0.6.0",
  "fs": "0.0.1-security",
  "gulp": "^3.9.1",
  "gulp-autoprefixer": "^3.1.1",
  "gulp-cssnano": "^2.1.2",
  "gulp-imagemin": "^3.3.0",
  "gulp-plumber": "^1.1.0",
  "gulp-rename": "^1.2.2",
  "gulp-sass": "^2.3.2",
  "gulp-sourcemaps": "^2.6.0",
  "gulp-uncss": "^1.0.6",
  "gulp-util": "^3.0.7",
  "include-media": "^1.4.9",
  "modularscale-sass": "^3.0.2",
  "nconf": "^0.8.4",
  "node-sass-asset-functions": "0.0.9"
}

Things to note in regards to Sass is the most recent alpha-version of Bootstrap V4, as well as Compass, include-media, and Modular Scale. These are all excellent libraries for simplifying the structure and styling of CSS, by using cross-browser standards and elegant helper-functions. The Gulp-libraries are for compiling the Sass and minifying assets. Notably, Autoprefixer for excellent cross-browser compatible CSS-declarations, CSSNano for optimal-minification, and UnCSS for aggressive CSS-removal. Additionally, “fs”, “nconf”, and “flickrapi” are used for filesystem-access, configuration-files, and Flickr-API access, respectively.

Where possible I avoid superfluous HTTP requests, for example when including images. If the image is fairly small, I base64-encode it to obtain a data-URI that can be placed directly in the CSS. This is also done for SVG-files.

Cleaning the CSS

UnCSS is particularly important here. This tool examines all used CSS-selectors from a set of files and removes all selectors not in use. You might think this sounds error-prone and unnecessary, but used intelligently it’s the most efficient reduction of a CSS-file possible. The reason for this is two-fold: Only explicitly needed selectors are kept and when something is left out, you can tell UnCSS to ignore a selector to keep it in. This is necessary for certain onclick-effects and the like. Since Grav compiles dynamic PHP-files with its cache, I use Wget to create a static copy of the development-site, olevik.dev, that UnCSS can analyze. I call Wget through a Gulp-task like this:

gulp.task('wget', (code) => {
  return cp.spawn('C:\\ProgramData\\Wget\\wget.exe', [
    '--recursive', 
    '--html-extension', 
    '--page-requisites', 
    '--convert-links', 
    '--no-host-directories', 
    'http://olevik.dev'
    ], { cwd: 'C:\\Caddy\\OleVik\\static', stdio: 'inherit' })
    .on('error', (error) => gutil.log(gutil.colors.red(error.message)))
    .on('close', code)
});

Here, cp.spawn is used to create a child process from Node. The produced HTML-files are an exhaustive rendering of what the site is actually producing, and thus optimal for use with UnCSS. Of course, to ensure that needed selectors are not removed, pages must be tested and UnCSS’s ignore-parameter optimized. I’d use Visual Regression Testing if the site was large or complex enough to warrant it.

Asset Manager

To avoid render-blocking CSS I use loadCSS, which loads these assets only after HTML in the DOM has finished rendering. Simply put, it applies an onload-attribute for <link>-tags that enables the stylesheet. The loadCSS-library is basically a polyfill for this behavior, which in this case enables deferred loading of app.css and Google Fonts. In Twig, I have implemented it this way:

{% do assets.addJs('theme://js/loadCSS.min.js', {'loading': 'inline', 'group': 'critical'}) %}
{% do assets.addJs('theme://js/cssrelpreload.min.js', {'loading': 'inline', 'group': 'critical'}) %}
{{ assets.js('critical') }}

{% block stylesheets %}
  {% do assets.addCSS('theme://css/app.css', {'group':'preload'}) %}
  {% do assets.addCss('//fonts.googleapis.com/css?family=Roboto:400,500|Antic+Didone|Lato:400,400i,700,700i|Source+Serif+Pro:400,600,700&amp;subset=latin-ext', {'group':'preload'}) %}
  <noscript>
    <link rel="stylesheet" href="{{ url('theme://css/app.css') }}">
  </noscript>
{% endblock %}
{{ assets.css('preload', {'rel': 'preload', 'as': 'style', 'onload': "this.rel='stylesheet'"}) }}

Specifically, the “critical” JS-group inlines loadCSS before all else in the <head>-tag. Next, the main stylesheet and fonts are loaded in a deferred-manner. Note that both of these stylesheets are single files, which reduces HTTP requests. I also include the standard <noscript>-tag to accommodate outdated browsers, or where JS is disabled. Before closing the <body>-tag I am including highlight.js for code-highlighting:

{% block scripts_end %}
  {% if page.header.highlight.enabled %}
    <script 
      type="text/javascript" 
      defer 
      src="{{ url('theme://js/highlight.pack.js') }}" 
      onload="hljs.initHighlightingOnLoad();"
    ></script>
  {% endif %}
  {% if page.header.highlight.lines %}
    <script 
      type="text/javascript" 
      defer 
      src="{{ url('theme://js/highlightjs-line-numbers.min.js') }}" 
      onload="hljs.initLineNumbersOnLoad();"
    ></script>
  {% endif %}
{% endblock %}

The same strategy is applied here: Defer loading the asset, and instantiate it onload.

Optimizing the server

I use PHP 7 for both development and production as it performs significantly better than previous versions. The server I host on also supports Gzip-compression, and I set Expires-headers to avoid having the browser load files unnecessarily. My host provides excellent quality services for a shared host, as well as excellent uptime and customer support in the rare case something goes wrong. They have pretty much the same system as a $10/month Droplet – and these technicalities do matter for Grav. The host uses LiteSpeed which is faster than Nginx or Apache, and I proxy the site through CloudFlare for free HTTPS – which also provides the speed of HTTP/2.

Results

As should now be clear, I’ve chased down the some of the most significant factors for the speed of the website. To evaluate the worth of all this optimization, I tested it against “A Grav Development Workflow”, which contains 1,567 words, two images, and a GitHub-Gist. The test yielded exceptional results: 100% PageSpeed score, 94% YSlow score, 0.9s time to fully load from the UK, and 89.1KB total page size. The site is already minimalistic, but as are the resources it needs to load.

The few exceptions from full scores come from minuscule differences in optimization between the test and my setup: 1-96 bytes of HTML and JS minification, a query string on app.css used to enable cache-busting, 219 bytes of optimization and lack of far-future Expiration-headers on external CSS by GitHub and Google Fonts, as well as a lack of CDN on these external assets. The remainder is from having three stylesheets, and cookies enabled on the site.

Even pages with much lower results than this should obviously ignore such details. There is no value in chasing down optimizations which won’t yield even a KB-reduction in page-weight, nor self-hosting external resource that are already optimized and hosted on CDN’s with better global distribution. With HTTP/2, the amount of HTTP requests does not make much of a difference, certainly not enough to warrant inlining large assets.

Comparison with a static site

Since I previously used Jekyll to generate the site, it is a natural candidate for comparison with Grav. From an empty browser-cache, with a hard reload to ensure assets are all reloaded, both Grav and Jekyll clock in at less than a second in initial load time and less than half a second subsequent load with browser-cache enabled. This is from my local Chrome-browser on the live server. A more structured comparison using WebPageTest, running 9 tests measuring initial and cached load times from Dulles, Virginia with Chrome, reveals:

Grav vs Jekyll

As seen, the static site Jekyll generates is faster in both cases when compared to Grav. Some of this will result from additional plugins running with Grav, but most of the difference will be made up from Grav actually being a dynamic system. Both versions clock in at less than 1.4s with an active browser-cache, which are very good results.

It is important to note, though, that the organizational structure of Grav is much better than with Jekyll. Content (pages), view (theme), logic (plugins), models and controllers (Grav) are clearly separated, and as mentioned Twig is a much more powerful and flexible templating-language than Liquid. Structuring and optimizing the Jekyll-site required a lot of workarounds and more time than doing the same with Grav, even when adding in new and improved elements.

Conclusion

Because I develop with Grav I would of course recommend it, but I would also note that compared to most other Content Management Systems it performs much better. From the examination in this post two things should be clear: A dynamic system can perform on line with a statically generated site, and using a task-runner like Gulp for theme-development can easily yield excellent results in regards to best-practices in web-development.

As outlined, the whole setup has a much clearer structure and separation of concerns, and with modern systems and optimization near-ideal results can be achieved.

optimization grav theme

Programmatic page-traversal in a Grav-plugin

For version 2 of my DirectoryListing-plugin I wanted to move away from the folder-traversal I had previously utilized. This post details how to programmatically iterate through Grav’s pages recursiv...