Low effort image optimization tips
Tags: webdev, performance
Published: 2024-03-26 19:12:22

“A picture is worth a thousand words”. So if a picture takes more than 4 seconds to load, does it mean that your website’s content fails to communicate a thousand words? In this blog post, we’ll learn how to identify unoptimized images, how to fix them, and how to validate the fix — so your website can speak volumes with highly-optimized images.

## The problem

I wish loading images was as simple as `<img src=”...”>`, but it ain’t. Loading images is a tricky thing, and it involves more than an `img` tag with a source attribute. Loading images in an unoptimized way can really hurt your page’s performance. See for yourself (screenshots are from [Sentry](https://sentry.io/welcome)):

![A trace view indicating bad performances due to unoptimized images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lttfju5yx8wqfk51ou5x.png)

Some of those images take *a lot* of time to load. Let’s see what actually we’re loading on that page:

![Large images showing up in Resources page in Sentry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6u87mg6sdbxeyhc4ztuy.jpg)

This is what a plain `<img src=””>` page looks like under the hood. Here are all the red flags we can see from the first screenshot:

1. The images are JPEGs (not the most website-friendly format nowadays)
2. They’re immediately loaded (not letting the browser display text content first)
3. They’re too heavy, so they take quite some time to load
4. The page took about 6 seconds to load

These three things caused the LCP to jump to ~4.2 seconds, the FP/FCP to 813ms, and also put too much work on the browser, which we can see from the many “Main UI thread blocked” spans (profiling revealed a connection between decoding images and long-running tasks on the main thread). Final verdict - **the website takes too long to load and it freezes up**.

Let’s now see how we can use image optimization to improve performance.

## Ditch JPEG for AVIF + WebP

As mentioned, JPEG is not the most website-friendly format nowadays. It produces larger files, doesn’t support transparency, and has visible blocky artifacts when exported with a lower quality. It’s not a modern image format.

Enter AVIF and WebP. Both AVIF and WebP are modern image formats. Compared to JPEG, they both produce smaller file sizes, have less visible blocky artifacts, and support transparency. Additionally, AVIF has support for higher color depth, so color accuracy is better. WebP on the other hand has much better browser support. At the time of writing this article, [only Internet Explorer does not have support for WebP](https://caniuse.com/webp).

How you can get to these formats varies by usage. If your images are static, you can use any of the image manipulation apps to re-export the JPEGs you might already have as AVIFs and WebPs. There are also web apps you can use to save yourself some storage space.

If your images are dynamic, or your website has a lot of images, then you have a few options.

### Generate images at build time

This approach is useful if you have all the images upfront and you would like to convert all of them before deploying your website. Bear in mind that it won’t work for dynamic images, since you won’t have them at build-time. You can use a Node library like [sharp](https://sharp.pixelplumbing.com/) to achieve this. Write a JS script that reads all the images in your project, converts them to AVIF and WebP, and puts them in a different folder in your project. (Tweet me at [@NikolovLazar](https://twitter.com/NikolovLazar) if you’re interested in seeing a more in-depth guide on this).

### Generate images on demand

In a similar fashion as build time, but instead move the logic into an API endpoint and perform the conversion on demand. This could be an endpoint that accepts the static image name/path (`/assets/lazar.jpg`), converts it on request and returns back the correct format. This approach allows you to generate only an AVIF image if the requesting client supports it, otherwise you can default to WebP. **Make sure to cache the results!** If you have the time, you could also upload the generated images to a CDN and serve those from that point on.

### Use a third-party service

Of course, if you don’t want to do all of this work above, you can opt-in and pay for a third-party service, like, [imgix](https://www.imgix.com/), [Cloudinary](https://cloudinary.com/), [Vercel’s Image Optimization](https://vercel.com/docs/image-optimization) (if you’re using Next.js), or others. Using these services in your app is a matter of implementing their SDKs and using their components instead of plain `img` tags

## Properly size images

Those “Main UI thread blocked” spans are nasty. Those are moments when the website is frozen. They happen because the intrinsic size of the images is a lot bigger than their rendered size, so the browser needs to spend time rescaling them on the fly. There are a couple of ways to go about this, and it involves tapping into the previous section’s mechanism.

If you **know the rendered dimensions** of the image, you can modify your generation mechanism (or image request) to scale up/down the image to the right dimensions.

If you **don’t know the rendered dimensions**, the easiest way to go about this is to simply make sure your images’ dimensions are within the ballpark of what’s possible. Let’s say your layout doesn’t span more than 768px horizontally, and that the images can be full width. The least you can do is to make sure that the images aren’t more than 768px wide. If the majority of your website’s traffic comes from mobile phones, figure out the largest width in that category and generate another set of images for it.

Either way, you’ll see improvements over using original images that are too large for your website.

## Use a `<picture>` element instead of just `<img>`

In the previous sections we mentioned generating multiple image formats and multiple image resolutions, but how do you put all of that together? Out with the `<img>`, in with the `<picture>`.

The `<picture>` element allows us to specify “image resources”. It still renders a single image, but it allows us to specify all the formats and resolutions we support, and leaves it to the browser to pick the best option.

```html
<picture>
  <source media="(min-width: 1024px)" type="image/avif" srcset="/optimized/large.avif" />
  <source media="(min-width: 350px)" type="image/avif" srcset="/optimized/small.avif" />
  <img alt="A photo" src="/optimized/fallback.webp" />
</picture>
```

This is how you can define a picture element that renders two AVIF images based on the viewport’s width, but also provides a fallback WebP image in case the browser does not support the AVIF format. This ensures that the browser will download the smallest possible image, which will improve the time to fetch the image and the time to render it.

If you opted-in to use a third-party service, look at its documentation to see if the platform itself supports automatic format/resolution, and if not, how you can specify the format and resolution.

## Choose lazy loading

When you’re loading your images directly impacts the [LCP core web vital](https://docs.sentry.io/product/performance/web-vitals/#largest-contentful-paint-lcp). You can change the way you’re loading your images through the `loading` attribute. There is no silver bullet solution, so when you should render your images **will depend** on a few factors.

If you **know the LCP element** - for example, a static image that you add to the page manually - you’re better off with `loading=”eager”` (which is the default btw). This tells the browser to start loading the image immediately, even before the First Paint event. The earlier the browser starts to load the image, the earlier it will finish loading, and the earlier the LCP event happens.

If you **don’t know the LCP element** (dynamic content), or for any images below the fold, you’re better off with `loading=”async”`. This tells the browser to defer loading the image until it’s in the viewport. For images below the fold, the LCP score won’t be affected since they don’t count, but also the browser won’t spend any time downloading them. For above the fold images that you can’t predict (dynamic content), you’re still better off loading them lazily. Here’s why:

* Dynamic content can cause multiple images to be loaded above the fold
* Dynamic content images can also mean different sizes and aspect ratios, which makes the LCP element inconsistent too
* Loading all of those images eagerly will slow down the page load, hurting your LCP score

If we were to add lazy loading to the `<picture>` element above, it would be like this:

```html
<picture>
  <source media="(min-width: 1024px)" type="image/avif" srcset="/optimized/large.avif" />
  <source media="(min-width: 350px)" type="image/avif" srcset="/optimized/small.avif" />
  <img alt="A photo" src="/optimized/fallback.webp" loading="lazy" />
</picture>                                          ^ set it on the img element
```

## The end result

So, if you apply all of the image optimizations above, you get a totally different picture:

![A trace view showing a significantly improved performances after optimizing the images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckjoxsl3vuigyzy24cv1.png)

Night and day! Here’s what you can see from the screenshot:

1. Images load significantly faster (AVIF + scaled down + lazy loaded)
2. LCP brought down to 363ms, while FP/FCP to 98ms
3. Significantly fewer UI thread blocks
4. The whole page load finished in 873ms

Converting the images to AVIF and downscaling them also means that the whole page will take a lot less bandwidth to load, which also means your page will be “cheaper” to load. If you’re a mobile user living in a country where data plans are expensive, this can be especially helpful.

So there you go. Four easy image optimization tips that can really make a difference in your website’s performance.

## Further reading

Performance: Web Vitals | Technical Demo - [https://www.youtube.com/watch?v=M0ROqRw2Fgs](https://www.youtube.com/watch?v=M0ROqRw2Fgs)
Performance: Web Vitals | Docs - [https://docs.sentry.io/product/performance/web-vitals/](https://www.youtube.com/watch?v=M0ROqRw2Fgs)
Performance: Resources | Docs - [https://docs.sentry.io/product/performance/resources/](https://www.youtube.com/watch?v=M0ROqRw2Fgs)