next.js: Image component `sizes` property does not work

Bug report

Describe the bug

When using the Image component, the sizes property doesn’t work as described in the docs.

When using the Image component, the image is resized to match the container width which is size of the width attributes that we passed, or fit 100% of the viewport if the image is larger than the screen, ignoring the sizes attribute completely.

Example of code:

<Image
      width="800px"
      height="456.8px"
      sizes="(max-width: 500px) 100px"
      src="/next.png"
      alt="Next.js"
    />

To Reproduce

I created a codesandbox example with the exact problem:

  1. Go to https://codesandbox.io/s/elated-neumann-u81mr?file=/pages/index.js
  2. Resize the browser window width to be bigger than 500px to see that the image is the size of width 800px and height 456.8px
  3. Resize the browser window width to be smaller than 500px to see that the image will fit to 100% of the screen, not respecting the sizes prop that should tell the image to be 100px

Expected behavior

I expect the image to have exactly a 100px when the view port is smaller then 500px, respecting the sizes attribute.

Screenshots

Bigger viewport width bigger-viewport

Smaller viewport width smaller-viewport

System information

  • OS: Ubuntu 20.04
  • Browser: Chrome
  • Version of Next.js: 10.0.0
  • Version of Node.js: v12.18.3

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 16
  • Comments: 47 (8 by maintainers)

Commits related to this issue

Most upvoted comments

Can we get this re-opened? If I’ve understood correctly, I don’t believe this is resolved.

The image component is incredible and a brilliant idea, but it seems responsive images are not behaving as expected.

When comparing @josepholiveira’s example here -

https://codesandbox.io/s/elated-neumann-u81mr?file=/pages/index.js

Expected behavior: I expect the image to have exactly a 100px when the view port is smaller then 500px, respecting the sizes attribute.

Actual behavior: Resize the browser window width to be smaller than 500px to see that the image will fit to 100% of the screen, not respecting the sizes prop that should tell the image to be 100px

To a pure html example, they should behave the same shouldn’t they?

Heres a codesandbox with both a next.js and html example side by side, trying to load the same size responsive images - https://codesandbox.io/s/empty-morning-4gdp9?file=/pages/index.js

export default function IndexPage() {
  return (
    <div>
      <Image
        width="1000"
        height="571"
        sizes="(max-width: 500px) 100px, (max-width: 1023px) 400px, 1000px"
        src="https://i.imgur.com/3QPVI5K.png"
        alt="Next.js"
      />
      <img
        srcSet="https://i.imgur.com/CLjcs9D.png 100w,
        https://i.imgur.com/8Jqeo06.png 400w, 
        https://i.imgur.com/3QPVI5K.png 1000w"
        sizes="(max-width: 500px) 100px, (max-width: 1023px) 400px, 1000px"
        src="https://i.imgur.com/3QPVI5K.png"
        alt="Next.js"
      />
    </div>
  );
}

The html example is inline with the way the expected behavior quoted above by @josepholiveira, in that it is supplying a 100px image.

The Next.js example loads a 320px image (as seen in network tab), and ignores the sizes request of a 100px image. (I’m also not sure why its 320, as that value is not present in deviceSizes or imageSizes).

I know when using layout=“responsive” next.js automatically supplies images that are sized based on the viewport, those images are at the following sizes: 640, 750, 828, 1080, 1200, 1920, 2048, 3840..

But is this ignoring of a requested size in the sizes prop intentional? Why could the image component not supply a 100px sized image? Loading a 320px image, that is 3.2x bigger than needed feels like a big hit to performance.

Maybe I am misunderstanding how responsive images or how the image component works, apologies if so. Thanks again for the great work! 😃

Thanks to @josepholiveira @styfle @AshConnolly & @LauraBeatris for your contributions here.

I finally understand how to use the sizes prop with Next.js. Here’s the simple explanation.

Can I add this to the docs via a separate pull request?

---------------------- ---------------------- ----------------------

Need a smaller size? Just specify a smaller visual width. The default is 100vw, meaning 100% of the viewport. For a 3-column layout, try 33vw – the closest image to 33% of the viewport will get served. You can also use media queries to get specific with your responsive image sizes. Don’t forget to add additional deviceSizes in next.config.js if you need sizes <640px for mobile devices. Otherwise, 640px will be the smallest size Next.js can serve. For convenience, you can specify a custom deviceSizes by simply adding the default imageSizes array (which goes down to 16px) before the default deviceSizes.

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

Once you add that code, you can conveniently use any small vw value (like 10vw) and know that there will always be an appropriate version on tiny mobile devices. The smallest served will be 16px, and you’ll have a total of 17 responsive image sizes served automatically by Next.js v10 or later.

---------------------- ---------------------- ----------------------

And here’s a bunch more info in case anyone finds this issue while searching online like I did.

Here’s an example that can be used with this Next.js + Tailwind CSS starter blog https://jamstackthemes.dev/theme/nextjs-tailwind-starter-blog

That starter blog has a nice overview of using Images in Next.js 10 here: https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs

Unfortunately it’s not clear what to do with a layout=“fill” image that won’t fill the entire viewport, without reading the MDN docs several dozen times.

(This obvious “use a sizes value of 33vw for an image in a 3-column grid” example does not appear in the MDN docs.)

Here’s a basic example of a small responsive image using 33vw (33% of the visual width of the device’s viewport):

index.js

          <div>
            <div className="relative h-36">
              <Image
                src="/static/images/ocean.jpeg"
                className="object-cover rounded-full"
                layout="fill"
                sizes="33vw"
              />
            </div>
          </div>

Here’s a full 3-column gird layout example that also has a long description of how you can use media queries in the string you pass as the “sizes” prop:

index.js

      <div className="max-w-3xl px-4 mx-auto sm:px-6 xl:max-w-5xl xl:px-0">
        <div className="grid grid-cols-3 gap-1 text-center md:gap-2 xl:gap-3">
          <div>
            <div className="relative h-36">
              <Image
                src="/static/images/ocean.jpeg"
                className="object-cover rounded-full"
                layout="fill"
                sizes="(min-width: 768px) 256px, (min-width: 1024px) 384px, 128px" // 128px used if width < 768px

                // Tailwind CSS explanation
                // max-w-3xl
                //   {max-width: 48rem/* 768 px */;}
                // xl:max-w-5xl
                //   @media (min-width: 1280px) {max-width: 64rem/* 1024px */; }
                // h-36
                //   {height: 9rem/* 144px */;}

                // These "sizes" result in almost the same behavior as 33vw.

                // A minimized Chrome window is 500px and will load the 128px
                // version. Later, when using 33vw, the 256px version will load.

                // That's because 33% (33vw) of 500px is 165px, so 256px loads.
                // (The resulting image is 148x144 when scaled with CSS.)

                // Note that this requires the following in next.config.js:
                /*
                module.exports = {
                images: {
                  imageSizes: [16, 32, 48, 64],
                  deviceSizes: [96, 128, 256, 384, 512, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
                },}
                */
                // You can inspect the image in the HTML code in Chrome in order
                // to find out its "intrinsic" size, as served by next.js.

                // Due to caching, you'll need to load a new private window at
                // the window size you want to test in Chrome.

                // You can also run Lighthouse in Google DevTools to test sizes.
              />
            </div>
          </div>
          <div>
            <div className="relative h-36">
              <Image
                src="/static/images/ocean.jpeg"
                className="object-cover rounded-full"
                layout="fill"
                sizes="33vw"
              />
            </div>
          </div>
          <div>
            <div className="relative h-36">
              <Image
                src="/static/images/ocean.jpeg"
                className="object-cover rounded-full"
                layout="fill"
                sizes="33vw"
              />
            </div>
          </div>
        </div>
      </div>

And here’s my Next.js config file:

next.config.js

module.exports = withBundleAnalyzer({
  images: {
    /* This is because imageSizes is only used when generating the 1x/2x/3x srcSet for layout="fixed" or layout="intrinsic".

    The deviceSizes are used for layout="responsive" and layout="fill" which generates a srcSet with all the device sizes. */
    imageSizes: [16, 32, 48, 64], // This array is concatenated to deviceSizes.
    // imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Next.js default
    deviceSizes: [96, 128, 256, 384, 512, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    // deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // default
  },
  // pageExtensions: etc.

Hope this helps someone. And @styfle if you approve I’ll open a pull request to make the changes to the docs. I don’t want to do it right this second, because I could be entirely wrong here 😅

@AshConnolly I’ve been dealing with a similar issue and I think I’ve figured some of this out. It’s what @styfle said above.

Basically the smallest image that Next generates by default is 640px. To allow Next to generate smaller images than 640px you need to create a next.config.js file at the root of your project and specify smaller device sizes. The code in your next config file should look like this in your case:

module.exports = {
  images: {
    deviceSizes: [82, 110, 140, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  },
};

Everything after the specific sizes you are looking for are the Next defaults.

Now you will have an image generated for each of those device widths. The sizes prop on the <Image /> are where you can override which viewport width will use which image source size which you have done. I think your code should just work as intended with those new device sizes.

Here is another example of the sizes attribute not being used as expected - https://codesandbox.io/s/restless-thunder-m8b9u?file=/pages/index.js

export default function IndexPage() {
  return (
    <div>
      <div className="pic-wrap">
        <Image
          src="https://i.imgur.com/zrJVdrt.jpg"
          alt="Next.js"
          width="140"
          height="210"
          sizes="(min-width: 1024px) 140px, (min-width: 475px) 110px, 82px"
          layout="responsive"
        />
      </div>
      <style jsx>{`
        img {
          width: 100%;
        }
        .pic-wrap {
          width: 82px;
        }
        @media (min-width: 475px) {
          .pic-wrap {
            width: 110px;
          }
        }
        @media (min-width: 1024px) {
          .pic-wrap {
            width: 140px;
          }
        }
      `}</style>
    </div>
  );
}

I am requesting an image that is

  • 82px on mobile
  • then 110px if viewport is 475px or wider
  • then 140px is viewport is 1024px or wider

The biggest I would want is 140px wide image. Next.js image component is supplying a 320px (28kb) image, for all sizes. That is vastly bigger than needed.

It also supplies a 640px when requesting a 82px wide image if i use layout=“intrinsic”: https://codesandbox.io/s/sweet-sanderson-fu2p0?file=/pages/index.js
(sometimes its 320px instead of 640px, not sure why it seems to change)

<Image
  src="https://i.imgur.com/zrJVdrt.jpg" 
  alt="Next.js"
  width="82"
  height="123"
  layout="intrinsic"
/> 

Why is this happening? A 640px image is vastly larger than an 82px wide image and will massively degrade performance.

Again, apologies if I am misunderstanding how responsive images or how the image component works.

I think it’s only supposed to work if you use the srcset attribute. Not sure though.

I thought of that too! But giving the fact that the Image component generates the srcset for you, and that the docs doesn’t provide any info on that attribute I preferred to open this issue, since it would make more sense to the Image component handle that too in that case.

@AshConnolly Your version of Next.js is very old, prior to the layout prop.

I tried your code and it works correctly with Next.js 10.0.5

<img
  alt="Next.js"
  src="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=256&amp;q=75"
  decoding="async"
  style="visibility: inherit; position: absolute; inset: 0px; box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
  srcset="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=96&amp;q=75 1x, /_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=256&amp;q=75 2x"
/>

Next.js doesn’t do any manipulation to the sizes property.

It is passed directly to the underlying img element, so that’s why the docs link to MDN.

The thing to remember is that sizes depends on srcset, and srcset is derived from deviceSizes in your next.config.js file. So make sure the width exists there.

Also, PR #19128 is going to autogenerate sizes when the prop is omitted to remain compliant.

I think this is still not fixed. I have the same issue. I’m simply trying to achieve max-width:100% effect we used in normal CSS.

From next.js perspective, I want something in the middle of intrinsic and responsive

From the docs:

When intrinsic, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports.

When responsive, the image will scale the dimensions down for smaller viewports and scale up for larger viewports.

The problem is, while using intrinsic next/image does not provide multiple device sizes like responsive (640, 720 etc) instead it just gives 1x & 2x only. When using responsive, it takes the whole container to fill in large screens. Not the max-width of image loaded.

So, how can I achieve something in-between?

Expected Result:

the image should scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports. but also generate srcSet defined from next.config deviceSizes (640, 720 etc)

If it’s supported already, can someone guide me? If not, it’s better to open this issue?

See Issue live on codesandbox

This should render at 200px maximum.

Thanks for replying @styfle.

You shouldn’t use sizes for layout=intrinsic.

Cool! I thought that seemed odd.

Unfortunately my issue is still present.

Can you clarify how would I go about getting the image component to return a small, non responsive image, at (for example) 82px width?

As seen here - https://codesandbox.io/s/sweet-sanderson-fu2p0?file=/pages/index.js

      <Image
        src="https://i.imgur.com/zrJVdrt.jpg"
        alt="Next.js"
        width="82"
        height="123"
        layout="intrinsic"
      />

The component is returning a 320px wide image when asking for a 82px wide image. generated html:

<img alt="Next.js" layout="intrinsic"
    data-src="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=320&amp;q=75"
    data-srcset="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=320&amp;q=75 320w"
    style="visibility: visible; height: 100%; left: 0px; position: absolute; top: 0px; width: 100%;"
    src="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=320&amp;q=75"
    srcset="/_next/image?url=https%3A%2F%2Fi.imgur.com%2FzrJVdrt.jpg&amp;w=320&amp;q=75 320w">

320px isn’t even listed in imageSizes:[16, 32, 48, 64, 96, 128, 256, 384]. It’s not in deviceSizes either, so I’m not sure where this value is coming from.

I’d expect the component to return a 96px wide image as that is the closest larger value in imageSizes.

On mobile devices with a 2x pixel density, if the component takes that into account (does it?), I’d expect a 256px image (the closest larger value, when the width is doubled for 2x pixel density).

If you could clarify how to do this, I’d appreciate it! 😃

These widths are used when the next/image component uses layout=“fixed” or layout=“intrinsic”.

This is because imageSizes is only used when generating the 1x/2x/3x srcSet for layout=“fixed” or layout=“intrinsic”.

The deviceSizes are used for layout="responsive" and layout="fill" which generates a srcSet with all the device sizes.

https://nextjs.org/docs/api-reference/next/image#sizes

I had the same issue but for now, as a workaround, I’m using the layout fill property with a container and media queries

<div className="md:h-20 md:w-20 w-10 h-10 relative">
  <Image
    quality={100}
    title="Floripa+"
    src="/images/logo.png"
    alt="Floripa Mais Logo"
    loading="eager"
    layout="fill"
   />
 </div>

In that way, the image won’t shift the content and always respect the size of the container. Therefore, I would prefer to use the sizes property

Thanks for getting back @styfle! 😃

Interesting… so adding more sizes to deviceSizes should solve my layout="responsive" issue. Ty & ty @cadekynaston!

As for my 2nd issue above with layout="intrinsic"

This is because imageSizes is only used when generating the 1x/2x/3x srcSet for layout=“fixed” or layout=“intrinsic”.

Why is srcSet used for non-responsive images? Also imageSizes contains values much smaller than 320px, but I keep getting a 320px image returned when requesting a small image (sandbox example in my previous comment is 82px wide).

Am I to believe that if i want to load a small, non responsive, image that is 48px wide (smaller than 320px which is often returned), I need to use layout="intrinsic", and pass it a sizes prop (which becomes srcSet for responsive images), even though the image does not need to be responsive?

That seems confusing and it doesn’t seem to work - https://codesandbox.io/s/billowing-bush-n3m0n?file=/pages/index.js

      <Image
        src="https://i.imgur.com/zrJVdrt.jpg"
        alt="Next.js"
        width="48"
        height="72"
        sizes="(min-width: 1024px) 48px, (min-width: 475px) 32px, 16px"
        layout="intrinsic"
      />

It still loads a 320px image: Screenshot 2021-01-12 at 20 12 56

So how would I go about getting the image component to return a small, non responsive image, at (for example) 48px width? As currently it keeps returning a 320px image.

Thanks for replying @cadekynaston! Appreciate it mate! 😃

That does seem to work, but it shouldn’t be needed as next already has an imageSize array by default, and it contains images that are much smaller than the 320px that is being loaded -

module.exports = {
  images: {
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
}

this is taken from - https://nextjs.org/docs/basic-features/image-optimization#image-sizes

You can specify a list of image widths using the imageSizes property. These widths should be different (usually smaller) than the widths defined in deviceSizes because the arrays will be concatenated. These widths are used when the next/image component uses layout=“fixed” or layout=“intrinsic”.

So it seems that next might not always be using this imageSizes array, and not be concatenating it with deviceSizes as explained.

@styfle are you aware of this issue?

Cheers! 👍

@josepholiveira Yep you’re right with an unstyled raw image tag the sizes attr does control the width. I agree the wrapper divs + default styles do put you in a situation where you can’t just drop in the new Image component without adding some extra styles.

@Mistwell The Image component generates a srcset based off the default device sizes. You can override the defaults if you want to allow the browser to choose images wider then 1200. (unrelated to sizes)

I mean, it’s the CSS implementation of object-fit / object-cover & the box model that’s “broken” because you have to play by specific rules to get CSS to figure out width & height in advance if they’re not specified directly on the image.

And since it’s incredibly easy to wrap a custom component around another React Component, you could implement that feature yourself if whatever way makes most sense for your use case without next.js have to force users into certain use cases.

I totally agree that it’s annoying to have to mess around & add a bunch of

<div>s just to have a proper "dynamic" image for the use cases I prefer, and some examples in the docs would be great.

You shouldn’t use sizes for layout=intrinsic.

The reason why layout=intrinsic has a srcset is for high DPI (retina) displays.

If you inspect the DOM or html response, you can see the srcset output.

Read more about srcset and sizes on MDN.

You’re welcome @AshConnolly! Yea I thought the same thing. Couldn’t get it to work with the imageSizes array at all.

As @mrmcc3 said

Next applies a list of device width breakpoints by default which is used to handle the cases where the images are wider than the viewport.

module.exports = {
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  },
}

For instance:

  • Viewport: 1200px
  • Image: 1920px

The final length of this image will be smaller than 1200px in order to fit the viewport

Refer to the intrinsic image layout, which is the default one:

If intrinsic, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports.

But if you want it to be fixed, regardless of device sizes or image sizes, the fixed property is the way to go:

If fixed, the image dimensions will not change as the viewport changes (no responsiveness).

sizes - Defines what proportion of the screen you expect the image to take up. Recommended, as it helps serve the correct sized image to each device.

It’s the intended size of the image so the browser can pick the best image from srcSet. From what I can tell it won’t actually style the image to be that size you’ve got to do that manually