Understanding the Sizes Attribute and Next.js Image Component

Introduction

Next.js' Image component is well documented, and also confusing, but why? One of the main reasons is because, under the hood, the <Image/> component uses native HTML responsive images, which are confusing.

So when you take something that already confuses people, and then add a swathe of features on top to cover all the potential use-cases, you need a lot of documentation to help all the confused developers.

This post explains some nuances that may have left you feeling confused, without knowing why, or wanting to invest the time into finding out why things are the way they are.

HTML image

Here's an example of a native HTML image.

<img/>

You need to at-least have a src attribute with a value, and for accessibility purposes, a description of the image in the alt attribute.

<img
  src="goat.jpg"
  alt="a cute goat screaming like a human"
/>

You can then enhance the experience by adding various sizes of the same image at different resolutions, this is called resolution switching.

<img
  src="goat.jpg"
  alt="a cute goat screaming like a human"
  srcset="goat-200.jpg 200w, goat-400.jpg 400w, goat-600.jpg 600w"
/>

If you add a width and height, and apply a dash of CSS to your images, they will also respect the aspect ratio of the image. The example below has an aspect ratio of 2/1.

<img
  src="goat.jpg"
  alt="a cute goat screaming like a human"
  srcset="goat-200.jpg 200w, goat-400.jpg 400w, goat-600.jpg 600w"
  width="400"
  height="200"
/>
img {
  max-width: 100%;
  height: auto;
}

Finally, you can add the sizes attribute.

<img
  src="goat.jpg"
  alt="a cute goat screaming like a human"
  srcset="goat-200.jpg 200w, goat-400.jpg 400w, goat-600.jpg 600w"
  width="200"
  height="200"
  sizes="100vw, (min-width: 768px) 200px"
/>

Providing you supply these attributes, the web browser takes the values and decides which image to serve based on the following criteria:

  • viewport width
  • width of media described by the descriptors, e.g. 400w
  • device pixel density

In the case of the previous example, if the viewport width is less than 768px, any of the images could be served. If the devicePixelRatio is 1, and the viewport width is 400px or less, goat-400.jpg is served, however, if devicePixelRatio is 2 goat-600.jpg is served instead as it has more pixels.

Above a viewport width of 768px I specified, using sizes, that the physical size of the image is 200px, that tells the browser to only get smaller images.

This complexity is what next/image is built on top of.

Next Image component

Next's <Image/> component uses some of the same props as <img/>, here are the ones you'll mostly use:

<Image width="" height="" src="" alt="" loading="" sizes="" />

Notice the missing srcset attribute?

One of the convenient features of <Image/> is that srcset is taken care of for you, automatically, based on what you provide via src.

By default, if you supply a src, width and height, this is the kind of srcset that is output.

<img
  srcset="goat.jpg&w=280&q=100 1x,
    goat.jpg&w=560&q=100 2x"
  src="goat.jpg&w=1120&q=100"
>

Due to the 1x, 2x descriptors, the browser's decision about which images to serve at different devicePixelRatio is already made,

My preference

I'm not a fan of using 1x, 2x, and prefer srcset to contain a granular list of images at various sizes appended by width descriptors e.g. 600w.

I then use sizes to inform the browser how big my images are at any given point, the information allows the browser to pick the most optimal image.

The default behaviour with next/image doesn't work like this, and can only be enabled by using the sizes attribute, but produces some odd results.

My preference in the following example is to use sizes, enter a value of "280px, (min-width: 768px) 420px", and get a srcset like this:

<img
  sizes="280px, (min-width: 768px) 420px"
  srcset="goat.jpg&w=16&q=100 280w,
    goat.jpg&w=32&q=100 560w,
    goat.jpg&w=32&q=100 1120w"
  src="goat.jpg&w=1120&q=100"
>

However, I get this:

<img
  sizes="280px, (min-width: 768px) 420px"
  srcset="goat.jpg&w=16&q=100 16w,
    goat.jpg&w=32&q=100 32w,
    goat.jpg&w=48&q=100 48w,
    goat.jpg&w=64&q=100 64w,
    goat.jpg&w=96&q=100 96w,
    goat.jpg&w=128&q=100 128w,
    goat.jpg&w=256&q=100 256w,
    goat.jpg&w=384&q=100 384w,
    goat.jpg&w=640&q=100 640w,
    goat.jpg&w=750&q=100 750w,
    goat.jpg&w=828&q=100 828w,
    goat.jpg&w=1080&q=100 1080w,
    goat.jpg&w=1200&q=100 1200w,
    goat.jpg&w=1920&q=100 1920w,
    goat.jpg&w=2048&q=100 2048w,
    goat.jpg&w=3840&q=100 3840w"
  src="goat.jpg&w=3840&q=100"
>

I can't do anything to stop that from happening, other than not using sizes 😭.

You might have never come across this, it seems like a hidden feature.

I stumbled upon it when I decided to use sizes, I didn't expect the vast array of images I got in the previous example.

The defaults cover a lot of bases, but I think this is an area for improvement. Imagine the HTML bloat on a busy product catalogue page, it's excessive, and completely unexpected.

The reason for the vast array of sizes is down the default configs that come from deviceSizes and imageSizes combined.

module.exports = {
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  }
}

They can be overridden in next.config.js, but its a blanket rule, I'd prefer to pass in a sizes value on a case-by-case basis, and it be used as the value, overriding the default config.

How they must have intended this

Next.js provide documentation on how to use sizes, but it's an "either, or" solution, and doesn't fit with how native responsive images work.

It's like they're recommending the default behaviour for the majority of cases, and using sizes is for other cases, but due to the way it works it's sub-optimal.

We shouldn't have to add a random value like 50vw to get a smaller srcset, that's just completely wrong.

This is from the docs:

the sizes property changes the behavior of the automatically generated srcset value. If no sizes value is present, a small srcset is generated, [...] If sizes is defined, a large srcset is generated [...] If the sizes property includes sizes such as 50vw [...] the srcset is trimmed to not include any values which are too small to ever be necessary.

next.js docs on next/image

The behaviour is unpredictable, and seems unintended, and there are a number of GitHub issues on the topic.

Conclusion

I'd prefer a single way to generate srcset; using w descriptors. It would be more flexible, less confusing, and less of a surprise feature.

I'd also like it if, when I use the sizes attribute I get an array of images that relate to the values used in sizes, and up-scaled versions of the same images at different resolutions.

Using sizes should be a front and centre feature, not something that feels like it's hidden away because it might confuse people, the end result is even more complicated.

Written by Morgan Feeney

I’ve been designing and developing for almost 2 decades.
Read about me, and my work.

For juicy tips and exclusive content, subscribe to my FREE newsletter.