Image Optimizations

Images are an important part of web development. They make the web page more appealing, but at the same time, they can also impact its performance if not optimized properly.

In this lesson, we'll cover some of the optimization techniques for web images, and talk about how to apply them to a Next.js project by utilizing its built-in <Image> component, which comes with many optimization features such as format conversion, automatic compressing, automatic resizing, lazy loading and so on.

Some image optimization techniques

We'll start with the general techniques. Optimizing images in web apps is a very important topic in web development. There are several things we must do:

  • Convert the image into a modern, web friendly format such as webp and avif.

webp and avif are both modern image formats designed to reduce file sizes compared to traditional formats such as JPEG and PNG, while retaining most of the original image quality. On average, webp is 30% smaller than JPEG and PNG, and avif is 50% smaller.

avif is smaller, but webp is more widely supported by most browsers, and offers faster decoding.

  • Reduce image file size to below 200KB.

Sometimes, just converting the image into a modern format is not enough to shrink the image file down to the optimal size, which is often below 200KB for web apps. In this case, you should consider reducing the image quality in order to further reduce the file size.

But of course, you must be careful with this, and make sure the images still have decent quality to be displayed on the website.

  • Create multiple versions of the image for different screen sizes.

Well, this is the most annoying part of image optimization.

Mobile devices usually don't require high-quality images due to the smaller screen size. And since these devices often have limited computing power, decoding large images can be slow, negatively impacting the overall user experience.

Large screen devices such as laptops and desktops, on the other hand, often need high-quality images with decent pixel density to maintain visual clarity.

To make sure the appropriate images are displayed for different devices, you need to create multiple versions of the same image:

text
1image-4x.jpg - 3000x2000 - 2MB    <===== Original image
2image-3x.jpg - 2100x1400 - 980KB  <===== 75% of Original image
3image-2x.jpg - 1500x1000 - 500KB  <===== 50% of Original image
4image-1x.jpg - 750x500 - 130KB    <===== 25% of Original image

And then use the srcset and sizes attributes to tell the browser which version should be displayed under different viewport.

html
1<img
2  src="image-4x.jpg"
3  srcset="
4    image-4x.jpg 3000w,
5    image-3x.jpg 2100w,
6    image-2x.jpg 1500w,
7    image-1x.jpg  750w
8  "
9  sizes="
10    (max-width: 600px) 750px,
11    (max-width: 1200px) 1500px, 
12    (max-width: 1800px) 2100px, 
13    3000px                      
14  "
15  alt="Example Image" />

Image with srcset and sizes

  • Lazy load the images.

Instead of downloading all the images the moment the webpage starts loading, you should only download the images that are visible to the user at initial load, and download the subsequent images as the user scrolls down.

This technique is called lazy loading. It helps minimizing the initial download size of the webpage, providing faster loading time, and better user experience.

  • Specifying the image size to minimize content layout shift (CLS).

And lastly, you should always specify size of the image by setting a width and height.

html
1<img
2  src="https://via.placeholder.com/300x200"
3  alt="Demo Image"
4  width="300"
5  height="200" />

This is because the content will always load before the images, and the browser must reserve enough spaces for the image.

If you do not specify a width and height, the browser will have to push the content down in order to fit the image, causing the page layout to shift and delivering a bad user experience.

Loading images with the Image component

Next.js comes with an <Image> component that can help us implement these optimizations. But first of all, we need to discuss how to load an image. For instance, this is how you can load a local image using <Image>:

text
1src
2├── app
3│   ├── error.jsx
4│   ├── favicon.ico
5│   ├── globals.css
6│   ├── layout.jsx
7│   └── page.jsx       <===== The page component
8├── components
9└── images
10    └── profile.png    <===== The image file

app/page.jsx

jsx
1import Image from "next/image";
2import avatarPic from "@/images/profile.png";
3
4export default function Page() {
5  return <Image src={avatarPic} alt="Avatar picture" />;
6}

When loading a local image, <Image> has two required props, src and alt. In the background, Next.js will automatically apply optimizations, such as converting the image into webp or avif format, generating multiple sizes of the image, generating the width and height of the image, and so on.

For the above example, the following <img> element will be generated:

html
1<img
2  alt="Avatar picture"
3  loading="lazy"
4  width="500"
5  height="500"
6  decoding="async"
7  data-nimg="1"
8  style="color:transparent"
9  srcset="
10    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fprofile.84cdbb12.png&amp;w=640&amp;q=75  1x,
11    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fprofile.84cdbb12.png&amp;w=1080&amp;q=75 2x
12  "
13  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fprofile.84cdbb12.png&amp;w=1080&amp;q=75" />

Alternatively, you can load the image from external sources, which is the more common setup when it comes to serving images. However it does require a bit more configurations, because Next.js has to process these images in the backend, and you must let it know it is safe to do so.

Go to the configuration file next.config.mjs, and add the following settings:

next.config.mjs

javascript
1const nextConfig = {
2  images: {
3    remotePatterns: [
4      {
5        protocol: "https",
6        hostname: "s3.amazonaws.com",
7        port: "",
8        pathname: "/my-bucket/**",
9        search: "",
10      },
11    ],
12  },
13};
14
15export default nextConfig;

In this case, we are telling Next.js it is safe to process the images coming from https://s3.amazonaws.com/my-bucket/.

And then, you can load images from this URL like this:

jsx
1import Image from "next/image";
2
3export default function Page() {
4  return (
5    <Image
6      src="https://s3.amazonaws.com/my-bucket/profile.png"
7      alt="Avatar picture"
8      width={500}
9      height={500}
10    />
11  );
12}

Notice that when loading images from remote sources, it is necessary to specify the width and height manually, because it is not possible for Next.js to know these information is the image files are hosted remotely. Most image hosting platforms offer API services that allow you to retrieve the dimensions of an image.

If there's no way for you to find out the exact size of the image, Next.js also provides alternative solutions with the fill prop, which we are going to discuss later.

Image format & quality

By default, the images will be converted to webp. Next.js will first check if the user's browser supports the format, if not, the original image will be sent.

You can enable avif support by specifying a formats option like this:

next.config.mjs

javascript
1const nextConfig = {
2  images: {
3    formats: ["image/avif", "image/webp"],
4  },
5};
6
7export default nextConfig;

In this case, Next.js will use webp as a fallback option. If the user's browser supports it, avif is usually 20% smaller than webp, but takes longer to encode, which means it will be slower the first time the image was requested, but all subsequent requests that are cached will be faster.

Besides specifying the image format, you can also define the quality on a per image basis.

jsx
1import Image from "next/image";
2import demoPic from "@/images/demo.png";
3
4export default function Page() {
5  return (
6    <>
7      <Image src={demoPic} alt="Demo picture" width={400} quality={100} />
8      <Image src={demoPic} alt="Demo picture" width={400} quality={75} />
9      <Image src={demoPic} alt="Demo picture" width={400} quality={50} />
10      <Image src={demoPic} alt="Demo picture" width={400} quality={25} />
11      <Image src={demoPic} alt="Demo picture" width={400} quality={1} />
12    </>
13  );
14}

The quality attribute accepts an integer from 1 to 100, where 100 means Next.js will retain the best image quality, but also the largest image file size. The default value is set to 75.

Images with different qualities

Different image sizes for different viewports

As we've discussed, the srcset and sizes attributes allow you to design a systems where images of different sizes are loaded on different screen sizes.

Smaller images will be shown on smaller screens to ensure best loading speed and user experience, and large images will be shown on bigger screens to ensure best visual appearance.

By default, Next.js generates a small srcset. For example:

jsx
1import Image from "next/image";
2import demoPic from "@/images/demo.jpg";
3
4export default function Page() {
5  return <Image src={demoPic} alt="Demo picture" />;
6}

The following <img> element will be generated:

html
1<img
2  alt="Demo picture"
3  loading="lazy"
4  width="6000"
5  height="4000"
6  decoding="async"
7  data-nimg="1"
8  srcset="
9    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75 1x
10  "
11  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75"
12  style="color: transparent;" />

Notice that there's only one item inside the srcset attribute, which means the same image will be shown for all screen sizes.

Next.js allows you to define a sizes prop, which, as you probably have guessed, matches the sizes attribute we discuss above.

jsx
1import Image from "next/image";
2import demoPic from "@/images/demo.jpg";
3
4export default function Page() {
5  return (
6    <Image
7      src={demoPic}
8      alt="Demo picture"
9      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
10    />
11  );
12}

A larger srcset will be automatically generated according to the sizes.

html
1<img
2  alt="Demo picture"
3  loading="lazy"
4  width="6000"
5  height="4000"
6  decoding="async"
7  data-nimg="1"
8  style="color:transparent"
9  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
10  srcset="
11    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=256&amp;q=75   256w,
12    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=384&amp;q=75   384w,
13    . . .
14    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=2048&amp;q=75 2048w,
15    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75 3840w
16  "
17  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75" />

Choose image according to viewport size

(Disable) lazy loading

All images will be lazy loaded by default, which means the image will only be loaded when it enters the viewport. However, the <Image> component comes with a loading prop, which allows you to customize this behavior.

jsx
1import Image from "next/image";
2import demoPic from "@/images/demo.jpg";
3
4export default function Page() {
5  return (
6    <>
7      <p>Lorem ipsum dolor . . .</p>
8      <Image
9        src={demoPic}
10        alt="Demo picture"
11        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
12        loading="eager"
13      />
14    </>
15  );
16}

When loading is set to eager, the image will be loaded immediately when the webpage is loaded.

Preloading the image

This leads to our next subject, what if the image is a very important part of the UI design, and you want it to be preloaded instead?

In this case, you can set the priority prop to true.

jsx
1import Image from "next/image";
2import demoPic from "@/images/demo.jpg";
3
4export default function Page() {
5  return <Image src={demoPic} alt="Demo picture" priority={true} />;
6}

And the following HTML tag will be generated in the <head> section of the webpage.

html
1<link
2  rel="preload"
3  as="image"
4  imagesrcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75 1x" />

This tag tells the browser the image resource specified by imagesrcset is a very important part of the webpage, and should be loaded as the first priority.

Also notice that priority={true} will automatically disable lazy loading. Notice that the loading="lazy" attribute is removed:

html
1<img
2  alt="Demo picture"
3  width="6000"
4  height="4000"
5  decoding="async"
6  data-nimg="1"
7  style="color:transparent"
8  srcset="
9    /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75 1x
10  "
11  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdemo.8f05840e.jpg&amp;w=3840&amp;q=75" />

Making image fill the parent container

As you know, width and height are two required fields for the <Image> component. When the image is loaded locally, the width and height are automatically generated. When the image is loaded from a remote sources, then you must provide the dimension information for that image.

However, there is one problem, what if you don't know the exact size of the remote image?

The fill prop is an alternative solution, which allows the image to automatically fill the parent container.

jsx
1import Image from "next/image";
2
3export default function Page() {
4  return (
5    <div className="relative w-40 h-32">
6      <Image
7        src="https://picsum.photos/seed/picsum/300/200"
8        alt="Demo picture"
9        fill
10      />
11    </div>
12  );
13}

The fill prop allows you to omit the width and height, but it does require the parent container to have position: "relative", position: "fixed", or position: "absolute" style.

The above example will generate the following result:

html
1<img
2  alt="Demo picture"
3  loading="lazy"
4  decoding="async"
5  data-nimg="fill"
6  sizes="100vw"
7  srcset=". . ."
8  src="/_next/image?url=https%3A%2F%2Fpicsum.photos%2Fseed%2Fpicsum%2F300%2F200&amp;w=3840&amp;q=75"
9  style="position: absolute; height: 100%; width: 100%; inset: 0px; color: transparent;" />

Notice that width and height are set to 100%, so the image size will depend on the size of the parent container in this case.