It makes logical sense that ‘faster’ pages are ‘better’ pages. They get things done faster, score better for SEO, use less energy and people like them. This is how I took a page important for SEO at mybuilder.com and cut its metrics in half.
What do we mean by ‘fast’
Page load is considered a ‘legacy metric’ at this point in the web performance community. This is because web pages are not just HTML anymore everything is parsed unpacked and many more things are downloaded. So in practice, a lower “page load” figure doesn’t necessarily correlate to a good user experience.
The first and largest contentful paint are two much better metrics to consider when trying to make a page that ‘feels fast’ as well as being objectively fast.
The first thing you should do is take stock of where your page is before you start. An easy way to do this is to use Chromes built-in Lighthouse feature or open up a SpeedCurve account and get familiar with the dashboard. This will give you an overview and a record of your progress, but to understand where to focus your efforts you will need details on exactly what is downloading, loading, rendering and requesting; and which order they are happening.
You can get Chrome to draw a programme chart of a page’s traffic known in web performance circles as a ‘waterfall’ due to its down and to the right look when the table is sorted by its default ‘time started’ column.
To see the waterfall for your page navigate to it in chrome then: Open the network tab in the developer tools Check ‘Disable cache’ Pick a less than perfect network condition e.g. ‘fast/slow 3g’ then hit refresh on the page.
When the waterfall is complete you should be able to see how items are bunched together forming obvious bottlenecks.
As I am new to the world of web performance I had two main strategies to speed the page up and a third that was taken care of by the MyBuilder SRE team:
- Have less stuff
- Reorder the stuff
- Faster servers
Have less stuff
If an image is not visible for some reason try and hunt it down and remove the offending code. Also, consider removing any images that are not working towards the page’s goal.
Compress your images, if you can’t visibly see the compression artefacts on your jpegs you are not squeezing hard enough. You will see the nasty squares but real users probably won’t. For PNGs run them through a png compressor which takes the ‘lossless’ format and applies a gzip like compression to the bits which will make images with lots of flat colours dramatically shrink in file size. SVGs are often small by nature but can be slimmed down with SVGO.
Once you have compressed the images make sure you are serving appropriate sizes to smaller devices. At MyBuilder.com we used Symfony and a twig function to look at the request and conditionally serve smaller images to ‘mobile’ user-agents. You can also do this with modern ‘responsive image’ HTML, which is probably better but is out of the scope of this post. This newer HTML markup will give the browser a load of options and specs for a set of images so it can decide which one it wants depending on its current conditions.
Images in the HTML are requested in the first batch because the HTML comes back from the initial server request but images in the CSS are displayed only if they are deemed visible after parsing the DOM and CSSOM (which can be blocked or forced to be re-evaluated by scripts). What this means is images placed with CSS background images end up coming comparatively late.
Webfonts slow down your page. Take a hard look and see if you need them all. Make sure the ones you do have are hosted from the same place as the rest of your assets. This will mean the client is already preconnected and can use HTTP2.
This one is less straightforward. It means having a proper look at what you are including and how much of that you are using. If you have jQuery included that is a good place to start but the road to removing it is out of the scope of this article.
If you can’t remove a script make sure it is in the right place. The traditional logic is to have vendor scripts in the document Head and ‘custom’ scripts after the page contents. The reason for this is the scripts in the head will be downloaded and parsed before the page is rendered but scripts after that will be downloaded when the browser reaches that line, which will block the page parsing until the script has run. A more consistent way to achieve this is to add either ‘async’ or ‘defer’ attributes to the script tag. ‘Async’ will stop the blocking but still run as soon as it can and ‘defer’ will wait until the page has completed its parsing.
Tree-shaking will analyse your code and only include functions or scopes that are called and will discard the rest. Great if you have something like Underscore.js included.
Code-splitting will split the compiled code into multiple bundles(files) which can be individually cached but more importantly, can be downloaded concurrently.
Scope hoisting is a newer method similar to module bundlers but will take the modules and put them into a single function after applying some re-naming to avoid conflicts. What this means is smaller files but also a more simple structure that uses less memory and can be parsed more quickly.
Re-order the stuff
The perception of a fast page improves when it looks ready to use sooner. Sometimes this means moving back an initial load to avoid showing something that is not ready to use.
When the background image starts to download the CSSOM has already been parsed and styles have already been applied and the page is rendering. This will appear as a space reserved for the image and it will pop in when it loads. Not only does this look bad but it pushes the ‘Last contentful pain’ further back in time.
To combat this background image pop-in we can make the browser aware that we will need this image sooner by adding a ‘prefetch link’. At MyBuilder.com the image we wanted to use was given to the Twig template to then put into a style tag, so we also added a prefetch link to the page head as well.
By default, the page will start to render the page before all the assets have finished downloading. In the case of the (large) webfonts this often causes a Flash of Unstyled Text (FOUT) which can cause a layout shift. To combat this you can tell the browser to wait until the font is loaded before starting by adding a preload link to make sure the client gets the asset before trying to render the page by adding a preload link to the asset.
<link rel="preload" href="https://www.example.com/assets/fonts/example-webfont.woff2" as="font" crossorigin="" type="font/woff2">
This will push back your initial render but will often produce a better user experience. You will have to make your own decision on the tradeoff.
<img src="tiny placeholder image" data-lazyimg data-src="assets/images/real-image.jpg" />
This can be applied to any image you think won’t be needed for the initial render.
I have already touched on HTTP/2. This ‘newer’ protocol can download more things from a single host at once. In the past, the client would hit the limit quickly and have to wait before it could get more assets from the same host. To get around this a hosting technique called ‘Sharding’ distributed your assets on multiple servers so the client could get more at once. With HTTP/2 this logic no longer applies and you are better off having everything in one place. You must make sure that your providers/servers are configured correctly or the client will revert to HTTP/1. At MyBuilder we use Cloudfront as our Content Delivery Network (a.k.a. CDN), which will cache the content at relevant Points of Presence (a.k.a. POP). This means that users will be able to get the content from a server closest to them geographically.
I am still new to the world of web performance but after making the changes above the page was roughly twice as fast with a ‘first contentful paint’ going from 8.32 to 3.38 seconds.