Today I Learned
When it comes to optimising images for the web, I don't tend to think too deeply about it. Generally, there are a few quick improvements that I make for images I upload to any project:
.webp.That covers me for 95% of the time; images are noticeably quicker and the load time is generally good.
The last 5%, however, is where some magic can happen to truly add sparkle to the User Experience (UX).
I might've done everything I can to slim down the file size of my hero image down or scale its dimensions to a smaller-than-ideal size, but it's still 350kb or 2000px wide. That'll cause that split second delay while the image is loading in the background - and that's noticeable even on a fast internet connection.
Can something even be done about that? Well, yes...
The idea behind Progressive Image Loading is that you show a much smaller, much blurrier image in the first instance so that your user can see something, because something is always better than nothing. Genius, right!
MDN have a fantastic quote that sums up this mentality:
"It's important to deliver something meaningful to the user as soon as possible — the longer they wait for the page to load, the bigger the chance they will leave before waiting for everything to finish."
The user sees something super fast (since this initial image is so much smaller and less detailed), and it progressively loads in the sharper, higher-quality image.
What does that look like in practise? Here's an example - notice how the image goes from blurry to crisp:

Better than an image that big suddenly appearing out of nowhere, I'm sure you'll agree!
What about implementation? Well, it's actually a lot easier than you might think...
<img
loading="lazy"
decoding="async"
src="low-resolution-img.webp"
data-highResSrc="high-resolution-img.webp"
class="lazy-image"
/>
.lazy-image {
/* Start blurry */
filter: blur(10px);
transition: filter 0.4s ease-in-out;
}
.lazy-image.loaded {
/* Remove blur once loaded */
filter: blur(0);
}
const imgs = document.querySelectorAll('.lazy-image');
imgs.forEach((img) => {
// 1. Grab the high-res URL from the data attribute
const highResUrl = img.getAttribute('data-highResSrc');
// 2. Tell the image to change the source to the high-res version
img.src = highResUrl;
// 3. Once the high-res file finishes downloading, trigger the visual transition
img.onload = () => {
img.classList.add('loaded');
};
});
<picture> and srcset? Don't those do the same thing?Almost! Using HTML's native <picture> element with its ability to list out a srcset does half the job - it only serves to make the actual image delivery quicker by displaying the correctly sized image for the device.
There's no point loading a 2000x1200px image on a mobile screen that's only 400px wide, so that's what it does: serve the right image for the right screen size.
But, progressive loading is all about the perceived speed. Research shows that users estimate download times based on active feedback rather than the actual, real-life time.
In summary, the ideal solution is a combination of the two - use <picture> to optimise the image delivery time, and progressively load the image to improve the user experience.
Direct from the source: Border Image Generator
Whether you're after dev work on a brand new site, an updated design, or just have a great business idea you want to get the ball rolling on, I'm here to help. Let's collaborate together!
Start a project