Silverstripe: Need for speed
- Publication
- Author
- Florian Thoma
- Categories
- Reading time
- 5 minutes
We have been developing Silverstripe websites for over a decade now and one thing that is always at the top of our minds is website performance. Since I wrote my last blog post about website performance, a lot has changed and it's time to have another look at how we can make a Silverstripe website really fast.
With our rebranding, we wanted to see what's possible and investigated every performance route known to us. We have implemented the new version of our website using as many of the tools we have in our toolbox as possible.
Even though a locally run Lighthouse report isn't representative of what users see in real life, but it was great to see that 100/100 Lighthouse score when we were testing our site.
Here are some of the lessons we learnt getting to that 100/100 Lighthouse score.
Overall Build
We strongly believe that websites should be server-rendered whenever possible. This takes advantage of the fundamental web technologies without loading megabytes of Javascript only to show a blog post or home page.
Of course, there are cases where a Javascript framework like React or Vue makes sense, but for brochure websites or blogs without complex app-like functionality, these frameworks are overkill and slow down a website unnecessarily.
That's why we server-render the whole website and don't use any Javascript or CSS framework. We use vanilla Javascript components where Javascript is necessary and we build all our CSS from scratch.
Javascript and CSS Requirements
In your controller, you can use the Requirements
class to add Javascript and CSS files to you page:
Requirements::themedJavascript('dist/js/main.js');
Requirements::themedCSS('dist/styles/app.css');
This will find the files in your theme and load it from your exposed _resources
folder:
<script type="application/javascript" src="/_resources/themes/simple/dist/js/main.js?m=1623671550"></script>
<link rel="stylesheet" type="text/css" href="/_resources/themes/simple/dist/styles/app.css?m=1627301274" media="screen">
Javascript files are render blocking by default. This means that the browser waits rendering the page when they encounter a Javascript file until that file is downloaded, parsed and executed.
Also, by default, Javascript files are added to the head of your HTML file. For older browsers we can push them to the bottom of the body so that the blocking occurs when most of the page is already rendered by using the following in PageController::init()
:
Requirements::set_force_js_to_bottom(true);
Newer browsers support deferring scripts until the whole of the page has been rendered. To do so, we need to change our code and pass it the defer option:
Requirements::javascript(
ThemeResourceLoader::inst()->findThemedResource('dist/js/main.js'),
['defer' => true]
);
This will add the defer
attribute to the script tag:
<script type="application/javascript" src="/_resources/themes/simple/dist/js/main.js?m=1623671550" defer="defer"></script>
We use gulp tasks to bundle and minify both Javascript and SCSS/CSS files to save bandwidth. We also split the CSS files for smaller and larger screens based on the media queries and load the one for larger screens using a media query in the media attribute.
Requirements::themedCSS('dist/styles/app.css');
Requirements::css(
ThemeResourceLoader::inst()->findThemedCSS('dist/styles/app-above-480.css'),
'screen and (min-width:480px)'
);
Result:
<link rel="stylesheet" type="text/css" href="/_resources/themes/simple/dist/styles/app.css?m=1627301274" media="screen">
<link rel="stylesheet" type="text/css" href="/_resources/themes/simple/dist/styles/app-above-480.css?m=1627301274" media="screen and (min-width:480px)">
Browsers still download the CSS, even if the media query doesn't match, but they assign it the lowest download priority.
Images
We use a gulp task to minify all images used in the theme, using gulp-imagemin. This minifies jpg, png and svg images during a build.
For icons, we create an SVG sprite, using gulp-svg-sprite. You can read more about SVG sprites here and here. To make sure that the SVG sprites load in all browsers, we use svgxuse.
For the images in the CMS we use the following modules to keep their size as small as possible:
- axllent/silverstripe-scaled-uploads: This module lets you scale down images that are uploaded through the CMS. When you never use images larger than say 1600px on your website, you can safely resize all uploaded images to a maximum of 1600px.
- axllent/silverstripe-image-optimiser: This module uses the command line tools JpegOptim, Optipng, Pngquant and Gifsicle to optimise all uploaded and resampled images.
- heyday/silverstripe-responsive-images: This module allows you to define sets of images that are then served through a picture element with different sources for different viewports.
On top of these, we implemented all images below the fold to be lazy loaded. We don't use the default lazyload functionality coming out in Silverstripe 4.9, but a custom implementation: In our theme, we replace the default DBFile_image.ss
template:
<img src="$PlaceholderImageURL($Width,$Height)" data-src="$URL.ATT" alt="$Title.ATT" height="$Height" width="$Width" class="lazyload" />
<noscript>
<img src="$URL.ATT" alt="$Title.ATT" height="$Height" width="$Width" loading="lazy" />
</noscript>
This sets the width and height of the images to make sure the aspect ratio is correct. For the placeholder image we have created a method PlaceholderImageURL()
that calculates the Greatest Common Divisor (GCD) of the width and height and generates a grey, base64 encoded data URL.
We then use lazysizes to load the images, and if no Javascript is available, we have the noscript
fallback using the native loading="lazy"
attribute. In the future, once most browsers are supporting the loading attribute, we will be able to remove the Javascript version and only use the native loading attribute.
Investigation into new Image Formats
We had a look into how we could deliver WEBP and AVIF images to the browsers that support them. If you can, you can install the Google PageSpeed module on your server that will take care of this (at least for WEBP). Unfortunately, that isn't an option on our server, so we kept digging.
There are a couple of Silverstripe modules available that create WEBP images from all uploaded images: nomidi/silverstripe-webp-image and tractorcow/silverstripe-image-formatter. But we didn't find them flexible enough and the generated images are not linked to the original images like other variants are.
There are two open issues here and here that would allow image manipulations to create variations with a different extension in Silverstripe core. These will eventually allow us to generate WEBP and AVIF variants of images.
For now, we just deliver optimised jpg and png files. But I'm looking forward to playing with the new manipulation options once they have been implemented in core.
HTML
When you build your templates using the templating system in Silverstripe, you end up with a lot of empty lines in the resulting HTML. That's mostly because every <% if $XYZ %>
that's on its own line leaves a blank line behind.
We have created a module that minifies the resulting HTML, using an HTTPMiddleware
: innoweb/silverstripe-minify-html
Website Delivery
Google recommends a server response time of less than 200ms, but Silverstripe 4 takes up to 1 second to render a page, depending on the modules you use. Elemental is one of the modules that give you great flexibility in building your pages, but it also takes time to render all the blocks.
Some parts of the site like the navigation can be cached using Partial Caching, but unfortunately, that is often not enough. The only way to get below the 200ms is to cache a static version of your pages.
We had a look at the static publishing module, but we found that a CDN does the same job with less work and better results.
As a CDN, we have chosen Fastly. (We used Section in the past, but unfortunately, their recent changes to network setup and pricing don't support small sites very well.)
We have implemented a module that integrates into the Fastly API: innoweb/silverstripe-fastly. With this, we can cache pages, images and other assets on the CDN. When pages and images are published, their cache is soft-purged through the API, so that the new version is loaded for future users.
At the moment Silverstripe core doesn't terminate the session and delete the login marker cookie after logout, but we have submitted three pull requests to get that fixed. Until that's in core, the site cookies need to be deleted manually after logging out of the CMS to get the pages served through the CDN again.
Conclusion
I hope this overview of the things we did to get a really fast Silverstripe website will give you some ideas on how to improve your website's speed.
Remember that every website is different and will need different tweaks.
Let us know if you need help with your Silverstripe performance or just want to chat about our learnings.
It has been awesome working with you on these [performance] updates. If at any point anyone is ever looking for a reference for site performance in the Silverstripe area we’ll definitely be pointing them in your direction.