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:
- Build stylesheets using SCSS
- Build a static copy of the site using Wget
- Clean the CSS using UnCSS
- Minify the CSS
- Losslessly minify images
- 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&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:
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.