How to Optimize Next.js Images

Ifeoma Imoh

The Next.js team at Vercel introduced their new Image component in 2020 — a more improved version of the traditional HTML img tag but with several SUPERPOWERS offered out of the box.

In this post, we'll look at three common problems with image optimization, and then we'll see how to solve these problems using the Next.js Image component in a simple web app.

Here is a link to the demo on CodeSandbox

Setting up the Project

Create a new Next.js application using the following command:

1npx create-next-app image-optimization

Image Optimization Concerns

Before we continue, let's have a look at some of the main concerns that demonstrate the need to optimize images, which are as follows:

  1. Image Quality: Because images can be used for various purposes, they are often captured using cameras in RAW format at very high quality, size, and resolution. While high-quality images are preferable for print, they sometimes increase the load times when used on the web, leading to a poor user experience. Quality optimization aims to slightly reduce image quality by compressing it — a compromise between image size and visual quality. Still, we also need to consider the viewing device to ensure the images are crisp enough when delivered.

  2. Image Format: There are several image formats, each with its compression levels, output quality, encoding, and decoding rates. Newer formats, such as avif (AOM) and webp (Google), allow high compression limits for high-resolution images, resulting in images with suitable quality and much smaller file sizes, up to 30% less than JPEG and PNG formats. It is important to serve images in the correct format to the requesting browser to ensure your website receives a high Core web vital score.

  3. Image resolution and resizing: Using traditional CSS to resize our images to ensure they are displayed accurately on a specific screen is acceptable. The major drawback to this approach is that we continue to serve the same large image intended for a larger device on a smaller one. To address this problem, we must deliver the appropriate image to the appropriate device, ensuring that optimal bandwidth is used to improve performance. We can take advantage of the srcSet and sizes attributes of the traditional img element or use the picture element, as we will see later.

  4. Image Metadata: This refers to the non-visual data included in the image, which could be data added by the camera used to capture the image (typically in EXIF format) or by photo editing software. According to Dexecure, image metadata accounts for around 16% of the file size, excluding specifically copyrighted information or necessary metadata, such as those needed by the browser to orient the image properly. Most image metadata should be removed to optimize image loading times, and fortunately, this is done by media optimization APIs used by Next.js.

Now that we have a decent understanding of what we're going to accomplish, we also need to be aware of some of the decisions made in this guide:

  • All our examples will be using the Google Chrome browser.

  • The Next.js image component supports both remote and static images, and all the optimization concerns we will be handling apply to both image sources, but in this post, we will be using remote images from Cloudinary.

Update your next.config.js file to look like so:

1module.exports = {
2 images: {
3 domains: ["res.cloudinary.com"],
4 },
5};

You must set the domains property if you are serving remote images and ensure that the protocol portion — http:// is not included.

Viewing an Unoptimized Image

Let's start by looking at the unoptimized image to see how it is before optimizing it based on the different factors we mentioned earlier.

Update your index.js file with the following:

1import Image from "next/image";
2export default function Unoptimized() {
3 return (
4 <>
5 <Image
6 src={
7 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651693700/sample_wx7dhx.jpg"
8 }
9 alt={"sample file"}
10 unoptimized={true}
11 height={500}
12 width={800}
13 layout="intrinsic"
14 />
15 </>
16 );
17}

In the snippet above, we import and render the Image component, and it receives the following props:

  • src: since we are using a remote image, the src attribute defines the URL to the desired image. Here, our URL points to a 6240 by 4160 image.
  • unoptimized: this flag, when set to true, will tell Next.js not to perform any optimization on the image and serve it as it is in its original format and quality.
  • width and height: this defines the dimensions of the image in pixels.
  • layout: defines the layout behavior of the image in relation to its viewport.

Start your application on http://localhost:3000/ using the following command:

1npm run dev

As seen in the image above, the unoptimized jpeg image has a size of 2.0MB.

Optimizing Image Resolution and Size

The srcset and sizes image attributes make it easy to solve the popular resolution switching problem and ensure that we serve responsive images. To achieve this, we can do one of two things:

  1. We can generate images with different resolutions to be rendered based on the end device DPR (device pixel ratio) — the number of the physical pixel on the device that makes up a single CSS pixel.

In your /pages folder, create a file called different-res and add the following to it:

1import Image from "next/image";
2export default function DifferentRes() {
3 return (
4 <Image
5 src={
6 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651694150/sea_huykbx.jpg"
7 }
8 height={500}
9 width={800}
10 alt={"some sample file"}
11 layout="intrinsic"
12 sizes="(max-width:3000px) 10vw"
13 />
14 );
15}

To view the image, start your application and navigate from the browser address bar to /different-res. Check the dev tools, and you should see an img tag with the srcset attribute populated with different resolutions for the image and the associated pixel density descriptions, as shown below.

  1. Secondly, we can generate different sizes for a single image so that the correct size is rendered based on the viewing device’s DPR and image resolution. In your /pages folder, create a file called res-diff-sizes.js and add the following to it:
1import Image from "next/image";
2export default function ResDiffSizes() {
3 return (
4 <>
5 <Image
6 src={
7 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651694150/sea_huykbx.jpg"
8 }
9 alt={"some sample file"}
10 height={500}
11 width={800}
12 layout={"responsive"}
13 sizes={"100vw"}
14 />
15 </>
16 );
17}

In the snippet above, we included the layout prop, set to responsive, and the sizes prop, which has a default value of 100vw but is set non-the-less for the sake of clarity. To clearly understand the essence of these props, open the running application, and from the browser address bar, navigate to /res-diff-sizes. Within the dev tools, take a look at the rendered image. You should get something like the image displayed below.

The Next.js Image component renders an image with the src, srcset, and sizes attributes. Because we set the layout mode to responsive, the srcset attribute is populated with different resolutions of the original image.

This is the default list of device width breakpoints provided for the images.deviceSizes property:

1module.exports = {
2 images: {
3 deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
4 },
5};

If you know the expected device widths of your users, you can modify this list in your next.config.js file to include the breakpoints, but the default list is okay in our case.

This list of breakpoints is dependent on the sizes property to determine what image size is rendered. By default, when we set the layout prop to responsive, except when explicitly set, the sizes prop defaults to 100vw, meaning that it checks the width of the device viewport and its DPR and based on that, it selects the appropriate image to be downloaded and rendered by the browser.

If you expect the image to have a size that will be less than the device viewport, you can specify the sizes prop, e.g. sizes="50vw". Using the images.imageSizes property, you can also specify a list of image widths in your next.config.js file. The default configuration looks like this:

1module.exports = {
2 images: {
3 imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
4 },
5};

The srcset attribute will be populated with an array of sizes generated by concatenating these widths with the array of device sizes. See here for more in image sizes.

Optimizing Image Quality

You may or may not have observed that the quality request parameter (q) with a value of 75 was included in all the image URLs generated by Next.js in all the previous images. The Next.js Image component includes a quality prop that can take any value from 1 through 100, with lower values offering higher compression and lesser file size.

Create a file called quality-optimization.js in your /pages folder and add the following to it:

1import Image from "next/image";
2export default function QualityOptimization() {
3 return (
4 <>
5 <Image
6 src={
7 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651694150/sea_huykbx.jpg"
8 }
9 alt={"sample file"}
10 height={500}
11 width={800}
12 layout={"responsive"}
13 sizes={"100vw"}
14 quality={50}
15 />
16 </>
17 );
18}

Here, we set the value of quality to 50. If you start your server again and navigate to /quality-optimization, you should see the rendered image.

Image Format Optimization

As mentioned earlier, different image formats allow for different file sizes and compression levels. Different browsers also support different image formats, which they include in their Accept Request Header. When a request for an image is made, for example, for Google Chrome, these headers look like this:

The Next.js image optimization API uses this list of the image mimetypes to identify the most appropriate representation that the requested image asset can be encoded in based on the configuration specified in the next.config.js file. In the next.config.js file, you can specify the images.formats property, an array where you can add a list of other image format mimetypes -- you can only specify a maximum of two entries.

1module.exports = {
2 images: {
3 domains: ["res.cloudinary.com"],
4 formats: ["image/webp"],
5 },
6};

As shown above, this means that the default media optimization API will check the browser Accept header to see if image/webp is included in it. If it is, it encodes the image to webp; else, it checks the next entry in the array, and precedence is important here. You can also include support for other newer formats like avif like so:

1images: {
2 formats: ['image/avif', 'image/webp'],
3 }

Image Load Priority

Different images on your site serve different purposes, such as background images, thumbnails, etc. This means that a specific use for an image may have higher priority over others. Next.js allows you to set priority to image assets by using the priority prop and setting it to true for one or more assets as needed. When this is done, the browser will attempt to load the image asset as soon as possible.

Create a file called image-priority.js in your /pages folder and add the following to it:

1import Image from "next/image";
2export default function ImagePriority() {
3 return (
4 <>
5 <Image
6 src={
7 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651694150/sea_huykbx.jpg"
8 }
9 height={500}
10 width={800}
11 alt={"sea"}
12 />
13 <Image
14 src={
15 "https://res.cloudinary.com/ifeomaimoh/image/upload/v1651233075/iy7b1t0appthwi9jkqs1.jpg"
16 }
17 height={500}
18 width={800}
19 alt={"cheerleaders"}
20 priority
21 />
22 </>
23 );
24}

In the snippet above, we included two images and passed the priority prop to the second image. If you navigate to /image-priority from the browser address bar, you will notice that the second image is loaded before the first, and to take a closer look, you can also inspect the network tab as shown below.

Conclusion

Image optimization is essential to web performance, and frameworks such as Next.js, in collaboration with large media optimization solutions such as Cloudinary, imgix, and others, have worked tirelessly to improve the developer experience when working with images. This guide provides a decent headstart to make good image optimization decisions when working with Next.js in your future projects.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.