Loading images like it's 2024
Jan 28th, 2024It'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:
<picture>
<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">
</picture>
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:
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.