Today I Learned

Progressive Image Loading

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:

  • Keep the image dimensions to realistic sizes.
  • Use a modern image format like .webp.
  • Compress the file size as much as possible with (free) tools like TinyPNG.

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...

Load those images... progressively!

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:

Progressive loading example

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');
  };
});

What about <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


Back to TIL

Let's grow your business,
together

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