Loading images like it's 2024

Loading images like it's 2024

Jan 28th, 2024

It's the new year, and that means that things have changed in web development. I'm going to give you the rundown on how to performantly load images in 2024.

Dynamic image sizing

The first step is making sure that you are serving the best image for your user. If they are on a tiny mobile device, they probably don't need to see your 2000px wide background image (as beatiful as it may be). To that end, the first thing we need to do is make sure that your images come in multiple sizes.

I highly recommend Cloudflare images when it comes to easily creating images at many different sizes. It's as simple as uploading your image (whether through API, S3, or console), and then you can dynamically resize the image based on a URI parameter. For example, if you have an image at https://images.sphuff.dev/my-image.jpg, you can render it at 500px wide by using https://images.sphuff.dev/my-image.jpg/w=500.

It's so much easier than resizing the image yourself.

If you don't want to go that route, the important thing is that you have differently sized assets for the major screen breakpoints. I like to use the tailwindcss breakpoints as my guide.

Ok, so now we have our image in different sizes - now what?

The picture element

The picture element is a new HTML element that allows you to specify multiple sources for an image, and allow the user's browser to fetch the one that fits its viewport size. Here's an example:

      <source srcset="https://images.sphuff.dev/my-image.jpg/w=2000" media="(min-width: 1024px)">
      <source srcset="https://images.sphuff.dev/my-image.jpg/w=1024" media="(min-width: 768px)">
      <source srcset="https://images.sphuff.dev/my-image.jpg/w=768" media="(min-width: 640px)">
      <img src="https://images.sphuff.dev/my-image.jpg/w=640" alt="My Image">

In this example, any screen larger than 1024px would see the largest image. Any screen between 768 and 1024 would see the next largest - you get the picture. This goes until we get the smallest screen size (which is also the fallback).

The great thing about this setup is that the user's browser only fetches the image size it needs. Your mobile device will no longer go with that beefy 2000px wide image - now it'll pick up the dainty 640px image instead. Since images are among your largest assets, this can make a huge difference in performance.

Lazy loading

Lazy loading is kind of a no-brainer. If your user doesn't need to see the image immediately, then wait until they do. The great thing is that this is now built into the browser, so you don't need to do anything special. Just add the loading="lazy" attribute to your image, and you're good to go. It's now at ~95% global adoption 🎉

Skeleton Images

Up until this point we've been working on improving actual performance - now it's time to chat about perceived performance.

Perceived performance may not show up on any pagespeed reports, but it keeps your users engaged and feeling like their momentary wait is going to be worthwhile.

A common pattern is to use "skeleton images" - loading a barebones version of the image before the actual. Many sites have a boring, gray rounded box with some kind of pulse animation:

Your boring loading animation

We can do a lot better. Let's take advantage of the fact that we have multiple image sizes, and load a tiny version of the image before the full image loads. We can then take that tiny image, pixelate with image-rendering: pixelated, and stretch it to create this cool loading animation:


That pixelated image you see is only 48x27 pixels (3.2 kb). It's a really cool effect for very little bandwidth that keeps your site feely speedy.

If you want to learn more, check out my post on how Letterboxd creates pixelated skeleton images.