Header image: Field notes and pencil by helloquence
I put out a tweet at the weekend:
And I thought that I'd explain just what was going on and how I'd managed to speed up page render time from almost 3 seconds down to 700 milliseconds without (and this is the key part) needing to use any kind of caching.
One of the things you need to know going into this, is that I'm not great at front end development - I'm about average at it, I'd say. Which is why this (what I eventually found out) was a bit of a eureka moment to me. But it also makes perfect sense.
The Site
Firstly you need to understand the original state of the site that I was attempting to optimise. First, the HTML which seems unassuming:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<link rel="stylesheet" href="dist/css/bootstrap.min.css">
<link rel="stylesheet" href="dist/css/custom.min.css">
</head>
<body>
<main class="page landing-page">
<section class="clean-block clean-hero" id="hero">
<div class="text">
<h2>Jamie "GaProgMan" Taylor<br></h2>
<p>.NET Core Developer; Podcaster; Writer</p>
</div>
</section>
</main>
</body>
</html>
Then some of the contents of custom.css
:
/* other rules, which override
* default bootstrap styles here
*/
#hero {
background-image: url('../img/header.jpg');
color: rgba(0,92,151,0.85);
}
custom.css
was designed as a file which overrides some of the default bootstrap styles, so it must be loaded in after bootstrap. Which is why the order of the css files is important.
All of this creates something which matches the following screenshot:
The Issue - And How Browsers* Render HTML
*
= some browsers anyway
I'm about to explain how browsers do a fetch, render, fetch, and repaint. I'm about to get the minutiae wrong, as I'm going to be doing it from a very high level. But the idea is, kind of, right.
So when the page is accessed, the above HMTL is downloaded. The browser starts to parse the HTML and sends off requests for the css files that are linked. With me so far?
Because bootstrap.min.css
is requested first, regardless of whether custom.min.css
comes back faster or not, the browser will parse and apply the rules from bootstrap.min.css
first. The browser will only apply the style rules from custom once bootstrap has been applied.
This means that the div with the "hero" Id will not have the background-image
rule applied until AFTER bootstrap has been downloaded and applied. Even then, the browser needs to then download the image, too. So we'll be waiting a long time for the image to download.
When I was testing this, on an intentionally throttled internet connection
I aim to design sites from a mobile first point of view; when I'm doing that, I test them from a mobile first point of view too. This usually means that I throttle my internet connection down to a low-quality 3G connection during my testing loop
I found that the header image would take, on average 2-3 seconds to load and appear.
Not good.
What's a Dev To Do?
I spent a little time thinking about what I could do. I made the header image smaller; I put it through tools like TinyPng; I moved the image to a very fast server; I even looked at using a caching reverse proxy service like CloudFlare.
I looked into using Brotli to compress the image at the server (but that wouldn't work on some browsers); I looked into using using the srcset
attribute; I even looked into using server-side caching.
Then I went for lunch.
When I got back from lunch, I stripped out bootstrap and realised that the bottleneck became how fast the server could serve the image. Which, over my throttled connection, took around 400 ms.
Wait a minute. Why would bootstrap be causing this?
It wasn't.
CSS File Order
Remember when I said earlier that we had to wait for bootstrap to be downloaded and applied before "hero"s background-image rule could be applied? Well, why not create a separate css file which just had that rule?
So I rewrote the HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<link rel="stylesheet" href="dist/css/header.min.css">
<link rel="stylesheet" href="dist/css/bootstrap.min.css">
<link rel="stylesheet" href="dist/css/custom.min.css">
</head>
<body>
<main class="page landing-page">
<section class="clean-block clean-hero" id="hero">
<div class="text">
<h2>Jamie "GaProgMan" Taylor<br></h2>
<p>.NET Core Developer; Podcaster; Writer</p>
</div>
</section>
</main>
</body>
</html>
and the only rule in header.min.css
was:
#hero {
background-image: url('../img/header.jpg');
color: rgba(0,92,151,0.85);
}
This meant that bootstrap didn't need to be loaded before the image had to be downloaded.
Great success! The hero banner and nav at the top of the page would load in around 600ms. Since the design placed these items (and nothing else) above the fold, it meant that I wasn't bothered about how long the rest of the content took to load.
But We Can Do Better
The CSS file was great, and super fast. But the browser had to make one more round trip to the server in order to download the background image. So how to we reduce that?
We can't assume that the server will support http2, which would allow us to push more content to the client in parallel. So what could I do?
Enter: Base64 strings
For those who don't know, Base64 is a way of encoding strings - it's in no way secure, but can be useful if you need to transmit binary data via an ASCII only medium.
You can take an image, encode it as a base64 string, and use that string as the value of the src
attribute for an image element. This technique does have it's drawbacks, tough - the chief two being:
-
browser support isn't perfect
- unless you don't care about targeting Microsoft browsers
- because of the sheer length of base64 representation of an image, it can take a little longer to download
- the base64 string for the screen shot of the rendered website design at the top of this post is 80,161 characters long for instance
BUT if you combine a base64 string with gzip, you can get pretty good transfer speeds. So I generated a base64 string for my hero image and altered the header.min.css
file:
#hero {
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUEBAQEAwUEBAQGBQUGCA0ICAcHC /* and the rest of the base64 string here*/ 5c1O5zUrxIMGEJWl6f/2Q=');
color: rgba(0,92,151,0.85);
}
This no longer required the extra round trip to the server, which shaved another 50ms from the initial download. With the added bonus that the css would be cached in the browser as soon as it was downloaded.
Reflections
I didn't really need to go the extra step (using a base64 string) but, if I'm honest with you, I was simply using it as an experiment to see whether it could be faster.
It's this kind of micro optimisation (moving simpler css rules above blocking calls) which can really make a huge effect on the perceived speed of your sites. Like I said at the top of this article, I'm not the best at front end dev, but thought I'd share this.
Also, I've probably not done this in a way that is even vaguely "correct" or an optimised way. If you were me, what would you have done?
Top comments (11)
Love seeing how a small change can make such a big impact.
Can I suggest an alternative that might be worth testing over the base64 encoding of the image?
Instead of reducing the number of round trips by encoding the image in base64, reduce the number of round trips by inserting your
header.css
directly in the<head>
of your document.With this version, your browser can parse the CSS straight away rather than fetching it and then start downloading the image file (which can also be cached) while moving on to start the download and parse of the bootstrap.css file too. With the base64 image you still have to download and parse the CSS, even if the CSS is mostly one image.
I'd be interested to know if that made a difference too.
That's a great suggestion, and I'll have to give that a try. I've no doubt that it will be much more well optimised than my solution of using a base64 image.
The one thing that would stop me from doing it in a "release" application would be that I really don't like using inline styles. It's not easy to change them globally (though in this example, it's pretty simple to do so).
My other reason for avoiding inline style blocks is that I've already added a CSP to this project, and it would mean adding either 'unsafe-inline' (which is horrendous for security) or a 'sha-...'/'nonce...' for the 'style-src' directive. I don't mind doing either, but when the inline style blocks change I'd have to remember to change the CSP.
EDIT:
I followed your advice, to see how the inline styling affected the page load times. Here is the page with the inline style block:
And here is the base64 version:
I'll have to make more changes to the way that the page is structured if I'm to use the inline style block I'm sure, and this was a really quick test (literally changing the one line, pushing to my test environment, and refreshing). But I'm looking forward to trying this again when I have more time
I like the look of the waterfall with the inline style! Though I can't see total page load time, it looks like it's an improvement.
You'd be surprised how many sites embed what they call the critical CSS (enough to style the initial page load above the fold) into the
<head>
of the page. Some of the faster sites in the world use this technique, places like the Guardian, the Financial Times and even this site.It stems from the work that many in web performance did a few years ago based on the critical path to displaying a web page, something you are talking about in this post. Namely that CSS must be downloaded and parsed before a browser will continue to render a page. For much more detail on this, and some real life results too, I recommend this talk by Patrick Hamann on CSS and the Critical Path. Then, for a bit more information on how you might go about implementing this, this post on Smashing Magazing by Dean Hume shows some techniques for using critical CSS. That might not cover tools that are in your toolchain, but it might give you some ideas. Particularly that it shouldn't necessarily change the way you write your CSS, but it may change that way that you serve it.
I understand the CSP issue, but it should be fairly hard to write insecure CSS and if you do go through the work to split up your CSS by critical and non-critical, it should follow that you can generate the
sha
hash required to satisfy your CSP.I am fascinated by web performance and seeing how techniques like this work as well as their real world results. Let me know if you do investigate this further!
You are not using bootstrap from a CDN so you shouldn't split your CSS into multiple files. Concat bootstrap.css and custom.css so you only have 1 css to download. Also, remove unused CSS via purge.css or other tools.
Give this a try with your original URL based CSS solution.
I really like the idea of using
rel="prefetch"
, I hadn't even thought of that.As for the CDN part, I'm currently trying to get this to be as fast as possible by hosting my own version of Bootstrap. I'm going to be looking into cutting out the stuff that I'm not using, so your tip about purge.css is greatly appreciated, too.
If your site is simple enough, ditch bootstrap. Nowadays it's not a huge help anyway.
Definitely.
If I was more well supported (i.e in Microsoft based browsers), I'd use flex grid because I've had a lot of success using it on my .NET Core Podcast website
95% for flex box :) More than enough.
88% for grid which is not that bad.
You can do most of the layouts via flex. But yeah, I can't wait to use grid for everything :)
Also, when you ditch bootstrap you can ditch jQuery, double win :D
Exactly this. I'm not a huge fan of jQuery and it's massive, so ditching that will reduce page load times by sooo much.
As an update to this: I spent a few hours re-writing the web app to use flex box with a few other optimisation suggestions here in the comments, and the application now renders (without a cache) in around 400-500 ms.
Just need to shave a little overhead off by utilising a better font. Maybe I should use Variable Fonts or something similar.
Wow, great job! I've always been rooted in the idea of having my one css file after bootstrap, and I never even considered splitting it up based on some smart loading.
Hm....need to run, I've got a couple tickets to write :)