The weight of images, whether they come from HTML or CSS, impacts the loading speed perceived by users when accessing your website. If things get too slow, you risk them abandoning their navigation and never getting to the content you’re providing. This is especially important if your website relies heavily on images.
Once you’ve made sure you were loading images at the proper sizes, both in the HTML and CSS, JavaScript can come to shave some more bytes to download for the browser and keep things fast by lazy loading images.
If your page has a large image far down its content, there isn’t any point loading it right when the page loads. It makes sense only to load it when the user is about to see it. Especially as users might just be interested in content that’s above that image and never reach it. That’s what lazy loading is about.
How it works
For images coming both from the HTML and CSS, the principle for lazy loading an image is the same. Instead of providing the browser with the actual image we want it to load at first, we give it a placeholder. Then, as the user scrolls the page, we detect when it is about to enter the viewport and replace its content with the actual one.
Because images will need a bit to be downloaded, we don’t want to do the swap right as they enter the viewport, but a while before (using the height of the viewport is a good starting point). Playing around with that buffer size, we can increase the chances for users to see the already loaded image as they scroll and not notice they are lazy loaded (though this will always be dependent on network conditions).
Regarding the placeholder, it can take various forms. The essential is too keep things tiny. For this specific image, here’s how a few options would look:
- A plain colour background (eg. the dominant colour of the image)
- A CSS gradient with average colours from the image
- A low-quality version image (potentially at a reduced size and blurred up by browser scaling)
- A blurred SVG generated from your image
Each provide a different experience to users, but also have different implementation costs. Generating a blurry SVG from a given image is trickier than resizing it to a low quality version, for example. The “best” solution will vary project to project, depending on the design direction, the technical stack you’re using, and more…
But enough with the theory, let’s see how it works in practice, starting with the images in the HTML.
For images in the HTML…
Unfortunately, browsers are super eager when it comes to loading images. As soon as they see an image with the appropriate src
(or srcset
and sizes
attributes), they’ll go ahead and start loading it.
To lazy load images, we’ll need to work around that. The images in the HTML will have no such attributes. Instead, the URLs for the actual images will be stored in data attributes and be swapped when the image needs to be loaded.
<img src="tiny-low-res.jpg" data-src="actual-image.jpg" data-srcset="[email protected] 2x">
As fun as it would be to implement that ourselves, there are already a few libraries taking care of the monitoring when images enter the viewport and do the swap for us. The jQuery Lazy plugin provides lazy loading with plenty of customising options, events, callbacks… However, as we’re in to save bytes here, if you only need basic lazy loading, lighter solutions exist.
The new IntersectionObserver API led to really lightweight libraries like lozad which will provide lazy loading for a few hundred Bytes (849B gzipped as of this article), plus a polyfill for older browsers (at the cost of 7ish KB gzipped).
We’re now equipped to lazy load the images in our HTML. Let’s see what we can do for the ones in the CSS.
… and CSS images too.
One of the places large images are often used is for background images, and these come from the CSS. Loading them later can provide substantial savings in bandwidth.
Most lazy loading libraries provide a way to lazy load a background image too, usually through a data-background
attribute or similar. However, they only allow providing one URL. This would ruin any work done on adapting the image size to the viewport width.
.withHugeBackground { background-image: url(small.jpg); } @media (min-width: 700px) { .withHugeBackground { background-image: url(medium.jpg); } } @media (min-width: 1400px) { .withHugeBackground { background-image: url(large.jpg); } }
We can use a similar method as the attribute swap for the HTML, instead adding a class (or data-attribute) triggering the rules with the actual images. Using a [custom loader for jQuery Lazy][custom-loader] or through an IntersectionObserver, for example.
We need to remember that images will take some time to get downloaded. If we just swap background-images, a white blink might show while the new image is loading. To prevent that, we can either:
- use multiple background images
.withHugeBackground { background-image: url(tiny-low-res.jpg); } .inViewport.withHugeBackground { background-image: url(actual-image.jpg), url(tiny-low-res.jpg); }
- use a pseudo element to cover the original background
.withHugeBackground { background-image: url(tiny-low-res.jpg); } .inViewport.withHugeBackground { /* Unless already positioned or with a z-index */ position: relative; z-index: 0; } .inViewport.withHugeBackground::before { content: ''; display: block; width: 100%; position: absolute; top: 0; bottom: 0; right: 0; left: 0; /* To display it below the content */ z-index: -1; background-url: url(actual-image.jpg) }
The second option opens the door to animating the apparition of the actual image, for example fading it in nicely (provided you do the work of detecting when the image gets loaded).
Lazy load all the things, then?
Lazy loading can save a substantial amount of downloads, or at least defer them until they are actually needed. It’s great, but shouldn’t be applied to every image and in every situation.
Some images will already be small enough (people avatars, tiny thumbnails, for example). Loading a reduced version of them first won’t gain much speed, and instead, we’d just add an extra download. For those, it’s fine to load them as usual. Equally, if your website doesn’t have much content, or large images far down the page, lazy loading won’t bring much either.
Another consideration is that the technique relies on JavaScript. No JavaScript, no actual image. There’s plenty of reasons the JavaScript might not reach your users’ browser, so it will have to enter the balance when deciding to use that solution. That said, it could be mitigated with a fallback embedded in the HTML doing the attribute/class swap after a timeout if the JS hasn’t loaded (with the appropriate <noscript>
fallbacks for users without JS at all).
With this article, we close the series on responsive images and how HTML, CSS and JavaScript can help keep the load times in check , so users get a fast experience. This leaves out a last area where more optimisation can be achieved: the images themselves. Lots of bytes can be saved by picking the appropriate format and optimising their encoding. This could lead to a whole new series of articles, but in the meantime, this guide covers the topic pretty extensively.
Image by Felix Russell-Saw on Unsplash.