Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

srcSet ignores sizes provided #27547

Open
samuelgoff opened this issue Jul 27, 2021 · 28 comments
Open

srcSet ignores sizes provided #27547

samuelgoff opened this issue Jul 27, 2021 · 28 comments
Assignees
Labels
bug Issue was opened via the bug report template. Image (next/image) Related to Next.js Image Optimization.

Comments

@samuelgoff
Copy link

samuelgoff commented Jul 27, 2021

What version of Next.js are you using?

11.0.1

What version of Node.js are you using?

14.15.4

What browser are you using?

Chrome Version 92.0.4515.107 (Official Build) (x86_64)

What operating system are you using?

macOS 11.5

How are you deploying your application?

Vercel

Describe the Bug

When I supply valid sizes, Nextjs ignores the intersection between these sizes and the deviceSizes & imageSizes contained in the next.config.js file. As a result, a file that will only ever appear on-screen at 280px (or 560px @ 2.0 pixel density, or 840px @ 3.0 pixel density) still generates every size, all the way up to 3840w.

Fortunately, the browser still requests the correct image for each screen resolution, but there are many sizes which are unreachable by any browser or device, and these are typically the largest, most resource-intensive ones to produce and maintain.

Expected Behavior

If provided, Nextjs should parse the sizes value to determine which deviceSizes & imageSizes are applicable to each image. Since both monitors & devices have a pixel density from 1.0 to 3.0 (or higher, though I recommend this as a sane limit), the smallest value in the sizes can be used as-is as a lower bound, whereas the largest sizes value should be multiplied by 3 to define an upper bound. Once these values are defined, they can be used as a floor/ceiling against the union of values in deviceSizes & imageSizes when generating scaled/optimized images. Values just outside of the target range may be worth generating, as well, so the browser has the flexibility to decide which image is most appropriate to request.

For example, given sizes="(max-width: 399px) 184px,(max-width: 519px) 244px,(max-width: 639px) 200px,(max-width: 767px) 156px,(max-width: 1023px) 220px,(max-width: 1279px) 280px,280px":

  • The smallest image size (at 1.0 pixel density) is 156px
  • The largest image size (at 1.0 pixel density) is 280px, but at 3.0 pixel density is 840px.

Given my current next.config.js include the following:

{
  images: {
    deviceSizes: [
      144,
      164,
      184,
      208,
      234,
      303,
      358,
      440,
      488,
      503,
      524,
      606,
      640,
      716,
      750,
      766,
      828,
      880,
      1080,
      1200,
      1920,
      2048,
      3840
    ],
    imageSizes: [
      16,
      32,
      48,
      64,
      96,
      128,
      256,
      384
    ],
  },
}

...the smallest file size that needs to be generated is 144px and the largest is 880px. Everything outside of that range can be ignored for the generation of this srcSet. In addition to the smallest, note this includes:

  • 1080
  • 1200
  • 1920
  • 2048
  • 3840

Each of these files takes time, processing & storage resources to generate and deploy. Eliminating them will deploy faster, require less processing, and require a smaller storage footprint. Additionally, the smaller srcSet will produce smaller HTML files, so they will download faster.

To Reproduce

Start with a source image provided at 3840px width and use import as recommended by the docs:

import missedTarget from '/public/images/missed-target.jpg';

Use next/image component with valid sizes smaller than the union of deviceSizes & imageSizes:

<Image
  src={missedTarget}
  layout='fill'
  objectFit='cover'
  placeholder='blur'
  sizes={[
    '(max-width: 399px) 184px',
    '(max-width: 519px) 244px',
    '(max-width: 639px) 200px',
    '(max-width: 767px) 156px',
    '(max-width: 1023px) 220px',
    '(max-width: 1279px) 280px',
    '280px']}
  alt='missed target' />

Optionally, provide a next.config.js including the following:

{
  images: {
    deviceSizes: [
      144,
      164,
      184,
      208,
      234,
      303,
      358,
      440,
      488,
      503,
      524,
      606,
      640,
      716,
      750,
      766,
      828,
      880,
      1080,
      1200,
      1920,
      2048,
      3840
    ],
    imageSizes: [
      16,
      32,
      48,
      64,
      96,
      128,
      256,
      384
    ],
  },
}

The rendered result will look like this:

<img alt="missed target" sizes="(max-width: 399px) 184px,(max-width: 519px) 244px,(max-width: 639px) 200px,(max-width: 767px) 156px,(max-width: 1023px) 220px,(max-width: 1279px) 280px,280px" srcset="/_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=16&amp;q=75 16w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=32&amp;q=75 32w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=48&amp;q=75 48w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=64&amp;q=75 64w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=96&amp;q=75 96w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=128&amp;q=75 128w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=144&amp;q=75 144w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=164&amp;q=75 164w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=184&amp;q=75 184w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=208&amp;q=75 208w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=234&amp;q=75 234w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=256&amp;q=75 256w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=303&amp;q=75 303w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=358&amp;q=75 358w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=384&amp;q=75 384w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=440&amp;q=75 440w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=488&amp;q=75 488w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=503&amp;q=75 503w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=524&amp;q=75 524w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=606&amp;q=75 606w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=640&amp;q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=716&amp;q=75 716w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=750&amp;q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=766&amp;q=75 766w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=828&amp;q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=880&amp;q=75 880w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=1080&amp;q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=1200&amp;q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=1920&amp;q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=2048&amp;q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=3840&amp;q=75 3840w" src="/_next/image?url=%2F_next%2Fstatic%2Fimage%2Fpublic%2Fimages%2Fmissed-target.4cee4f8c3b9383517209b12b7cc81b00.jpg&amp;w=3840&amp;q=75" decoding="async" style="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%; object-fit: cover; filter: none; background-size: cover; background-image: none;">

Note the srcSet includes all image sizes – all the way up to 3840 – regardless of whether they could possibly be displayed by any browser or device.

@samuelgoff samuelgoff added the bug Issue was opened via the bug report template. label Jul 27, 2021
@markplewis
Copy link

Ideally, there would also be a way to limit the number of srcSet sizes on a per-image basis. Perhaps a deviceSizes prop could be added to the Image component, which would override the values defined in next.config.js? One use case for this would be the following:

Imagine I have a large hero image, which I'm displaying at 1000px wide (on a 1.0 pixel density screen). Next.js generates a 3000px wide image to accommodate users with 3.0 pixel density screens. Even though this is within the range of sizes that could reasonably be displayed, I consider this image too large to justify serving, so I intentionally want to limit it to a maximum of 2000px and serve a lower-quality image to users with 3.0 pixel density screens.

Does this suggestion belong here or should I create a new issue/feature request for it? Thanks!

@samuelgoff
Copy link
Author

Hey @markplewis, thanks for reminding me. I forgot to mention: ideally Nextjs wouldn’t attempt to scale up the image above native resolution ever, because this will always be inferior to allowing the browser to scale up. Usually a Lancszos or bicubic algorithm will look worse than simply doing nothing to the image.

The only exception: if it used DLSS (deep learning super-scaling) to res up the imagine beyond its native resolution. I’ve used several of these cloud-based and desktop DLSS imaging solitons, and they can usually handle up to 400% up-res without compromising quality in most cases.

@samuelgoff
Copy link
Author

samuelgoff commented Aug 11, 2021

Ideally, there would also be a way to limit the number of srcSet sizes on a per-image basis.

If it’s working properly, the values you pass into sizes prop on each image should inform which srcSet values get generated. That’s sort of the point of my issue: it’s currently ignoring what sizes are relevant.

The idea of using a consistent set of device sizes (shared across all Image instances) is that your audience will be using the same exact devices to view every page and every Image on your site/app. The only variable would be: what sizes will the image appear at various breakpoints (or more specifically, what is the largest an image might appear at each breakpoint)? This is the intended use for the sizes prop.

@markplewis
Copy link

Thanks @samuelgoff, all of that makes sense. I'm working on my first NextJs project and I was surprised that every image uses the same, global set of sizes to generates its srcSet, and that there's no way to override this on a per-image basis. As you said though, when this feature is working properly, the sizes prop should restrict the number of srcSet images that are generated. So, I suppose I could do something like this to accommodate the hypothetical use case that I described above:

sizes="(min-resolution: 2dppx) 2000px"

@samuelgoff
Copy link
Author

Thanks @samuelgoff, all of that makes sense. I'm working on my first NextJs project and I was surprised that every image uses the same, global set of sizes to generates its srcSet, and that there's no way to override this on a per-image basis. As you said though, when this feature is working properly, the sizes prop should restrict the number of srcSet images that are generated. So, I suppose I could do something like this to accommodate the hypothetical use case that I described above:

sizes="(min-resolution: 2dppx) 2000px"

Hey @markplewis, bear in mind the browser has the luxury of using both srcSet and sizes together with what it knows about itself.

If you look at dppx on caniuse, this doesn't really have great coverage. At least in the past, the media queries supported by srcSet/sizes were pretty limited in complexity -- and very persnickety about making sure you sequence them from smallest to largest breakpoint (not the other way around), if you wanted it to work across all modern browsers. And unlike what we're used to expecting with overlapping media queries and specificity, browsers look to the sizes values and use the first valid rule. Hence my example of starting at the smallest breakpoint and setting max-width, then working my way up. This guarantees the browser will only request the smallest possible image that is appropriate for that resolution of device.

If it were me, I would write the sizes rules to use (regular) px & vw units, and let the browser decide to grab the entry in the srcSet that is 2x, 3x, etc. of the regular resolution. This trusts the browser to know its own device pixel ratio -- which it does -- and as long as the entries exist for the 2dppx version of an image in the srcSet, it will automatically request that URL instead of the 1dppx version. This is the strategy I've used in the past, and it works more reliably across more browsers and devices because it allow you to maintain the simplest possible media queries in the sizes.

@samuelgoff
Copy link
Author

Thanks @samuelgoff, all of that makes sense. I'm working on my first NextJs project and I was surprised that every image uses the same, global set of sizes to generates its srcSet, and that there's no way to override this on a per-image basis. As you said though, when this feature is working properly, the sizes prop should restrict the number of srcSet images that are generated. So, I suppose I could do something like this to accommodate the hypothetical use case that I described above:

sizes="(min-resolution: 2dppx) 2000px"

Hey @markplewis, Going back to your original use case, if the largest resolution you want something to appear in the srcSet being generated is 2000px, I would provide your own next.config.js file with a more limited set of images --> deviceSizes, so the largest value would be 2000. If it were me, I would use 2048 instead of 2000, because that jives better with how many screen resolutions actually exist based on powers of 2 (e.g. a 1024px screen with 2.0 pixel density = 2048dp).

Regardless, if you provide your own next.config.js file with the largest of 2000 inside images --> deviceSizes, then this should produce the result you are after.

@timuric
Copy link

timuric commented Oct 14, 2021

Not sure if this is related but for some reason if you provide a query with calc, like this: (min-width: 740px) calc(100vw - 100px) it will produce similar results:

/_next/image?url=%2Fvercel.svg&w=16&q=75 16w, 
/_next/image?url=%2Fvercel.svg&w=32&q=75 32w, 
/_next/image?url=%2Fvercel.svg&w=48&q=75 48w, 
/_next/image?url=%2Fvercel.svg&w=64&q=75 64w, 
/_next/image?url=%2Fvercel.svg&w=96&q=75 96w, 
/_next/image?url=%2Fvercel.svg&w=128&q=75 128w, 
/_next/image?url=%2Fvercel.svg&w=256&q=75 256w, 
/_next/image?url=%2Fvercel.svg&w=384&q=75 384w, 
/_next/image?url=%2Fvercel.svg&w=640&q=75 640w, 
/_next/image?url=%2Fvercel.svg&w=750&q=75 750w, 
/_next/image?url=%2Fvercel.svg&w=828&q=75 828w, 
/_next/image?url=%2Fvercel.svg&w=1080&q=75 1080w, 
/_next/image?url=%2Fvercel.svg&w=1200&q=75 1200w, 
/_next/image?url=%2Fvercel.svg&w=1920&q=75 1920w, 
/_next/image?url=%2Fvercel.svg&w=2048&q=75 2048w, 
/_next/image?url=%2Fvercel.svg&w=3840&q=75 3840w

Probably this is caused by the same bug

@samuelgoff
Copy link
Author

samuelgoff commented Oct 14, 2021

Not sure if this is related but for some reason if you provide a query with calc, like this: (min-width: 740px) calc(100vw - 100px) it will produce similar results:

/_next/image?url=%2Fvercel.svg&w=16&q=75 16w, 
/_next/image?url=%2Fvercel.svg&w=32&q=75 32w, 
/_next/image?url=%2Fvercel.svg&w=48&q=75 48w, 
/_next/image?url=%2Fvercel.svg&w=64&q=75 64w, 
/_next/image?url=%2Fvercel.svg&w=96&q=75 96w, 
/_next/image?url=%2Fvercel.svg&w=128&q=75 128w, 
/_next/image?url=%2Fvercel.svg&w=256&q=75 256w, 
/_next/image?url=%2Fvercel.svg&w=384&q=75 384w, 
/_next/image?url=%2Fvercel.svg&w=640&q=75 640w, 
/_next/image?url=%2Fvercel.svg&w=750&q=75 750w, 
/_next/image?url=%2Fvercel.svg&w=828&q=75 828w, 
/_next/image?url=%2Fvercel.svg&w=1080&q=75 1080w, 
/_next/image?url=%2Fvercel.svg&w=1200&q=75 1200w, 
/_next/image?url=%2Fvercel.svg&w=1920&q=75 1920w, 
/_next/image?url=%2Fvercel.svg&w=2048&q=75 2048w, 
/_next/image?url=%2Fvercel.svg&w=3840&q=75 3840w

Probably this is caused by the same bug

This could be unrelated, but the example image you use here reminds me of a separate set of issues: in general, a properly implemented SVG will tend to be smaller than PNG or WebP. For this reason, I've often inlined my SVGs instead of using the next/image component, to prevent next/image from harming my well-crafted SVGs. IMO, next/image should "first, do no harm" in terms of making an SVG larger by rasterizing it.

That said, it is entirely possible to encounter highly-detailed (or poorly-crafted) vector images (think distressed or grunge textures). I've seen examples where it's 50-150KB as a PNG, but over 1.4MB as an SVG. Ideally, next/image would profile by encoding SVGs as SVG+brotli and as PNG & WebP, and compare the results & pick the winner. This way, in most cases, SVGs would be preserved as SVGs & performance would be significantly better.

@timuric
Copy link

timuric commented Oct 14, 2021

Not sure if this is related but for some reason if you provide a query with calc, like this: (min-width: 740px) calc(100vw - 100px) it will produce similar results:

/_next/image?url=%2Fvercel.svg&w=16&q=75 16w, 
/_next/image?url=%2Fvercel.svg&w=32&q=75 32w, 
/_next/image?url=%2Fvercel.svg&w=48&q=75 48w, 
/_next/image?url=%2Fvercel.svg&w=64&q=75 64w, 
/_next/image?url=%2Fvercel.svg&w=96&q=75 96w, 
/_next/image?url=%2Fvercel.svg&w=128&q=75 128w, 
/_next/image?url=%2Fvercel.svg&w=256&q=75 256w, 
/_next/image?url=%2Fvercel.svg&w=384&q=75 384w, 
/_next/image?url=%2Fvercel.svg&w=640&q=75 640w, 
/_next/image?url=%2Fvercel.svg&w=750&q=75 750w, 
/_next/image?url=%2Fvercel.svg&w=828&q=75 828w, 
/_next/image?url=%2Fvercel.svg&w=1080&q=75 1080w, 
/_next/image?url=%2Fvercel.svg&w=1200&q=75 1200w, 
/_next/image?url=%2Fvercel.svg&w=1920&q=75 1920w, 
/_next/image?url=%2Fvercel.svg&w=2048&q=75 2048w, 
/_next/image?url=%2Fvercel.svg&w=3840&q=75 3840w

Probably this is caused by the same bug

This could be unrelated, but the example image you use here reminds me of a separate set of issues: in general, a properly implemented SVG will tend to be smaller than PNG or WebP. For this reason, I've often inlined my SVGs instead of using the next/image component, to prevent next/image from harming my well-crafted SVGs. IMO, next/image should "first, do no harm" in terms of making an SVG larger by rasterizing it.

That said, it is entirely possible to encounter highly-detailed (or poorly-crafted) vector images (think distressed or grunge textures). I've seen examples where it's 50-150KB as a PNG, but over 1.4MB as an SVG. Ideally, next/image would profile by encoding SVGs as SVG+brotli and as PNG & WebP, and compare the results & pick the winner. This way, in most cases, SVGs would be preserved as SVGs & performance would be significantly better.

The only reason I used svg was because it was a standard boilerplate demo. To be honest svg should not require srcset since the weight is size invariant (even if it embeds bitmap there is no way to optimize for most image cdns)

@styfle styfle added the Image (next/image) Related to Next.js Image Optimization. label Sep 22, 2022
@karlhorky
Copy link
Contributor

This also happens with the new Image component in Next.js 13

@JonnyBoy333
Copy link

I'm encountering one of the issues mentioned above where next.js is adding image sizes that are greater than the native image to the srcset (causing the image to get scaled up) when I'd really just like to serve the native image in that instance. If you use tools like https://ausi.github.io/respimagelint/ to validate you responsive images they will complain about this saying something like Descriptor 3840w doesn’t match the image size of 2736x2051 from ./path_to_image

@samuelgoff
Copy link
Author

This also happens with the new Image component in Next.js 13

That's truly disappointing. I was hopeful they would've addressed this issue in the rewrite, because it creates unnecessary bloat & slowdown when building/deploying -- and even makes the rendered HTML larger than it needs to be -- to assume that every image is going to be used full width. I think that's a huge & false assumption.

@samuelgoff
Copy link
Author

To Vercel's credit, they've added support for webp & avif, which makes the overall file size smaller (avif is roughly 50% smaller than JPEG, all things being equal), but they take significantly longer to encode. So encoding unnecessarily large images at full resolution (& down) actually makes the build/deploy performance issues I identified with v11 significantly worse.

@MartinDavi
Copy link

MartinDavi commented Jun 10, 2023

From the docs:

If the sizes property includes sizes such as 50vw, which represent a percentage of the viewport width, then the source set is trimmed to not include any values which are too small to ever be necessary.

From my next.config.js:

deviceSizes: [375, 768, 1280, 1440, 1920],
imageSizes: [64, 128, 192],

If I put sizes=100vw on an image, then values from imageSizes are omitted from the srcSet, since they are all smaller than my smallest declared deviceSizes.

sizes=50vw results in only 192px being merged in from imageSizes, since 375px / 2 === 187.5.

So that part of the documentation seems correct, it won't include sizes that are so small they would never be served based on vw given, and the smallest deviceSizes.

But if I do something like sizes=1vw or sizes=1px which are obviously ridiculous, all the srcSets are generated, even though the smallest imageSizes value will always satisfy.

So yes, to the original point a bunch of extra srcSets are placed in the HTML which definitely adds some needless bloat.

But here is my confusion:

Does next.js actually generate all of these images as soon as a single one is requested, or does it only generate the srcSet needed when requested. I suspect it's the latter, in which case your statement of:

Each of these files takes time, processing & storage resources to generate and deploy. Eliminating them will deploy faster, require less processing, and require a smaller storage footprint.

probably isn't true, since srcSets that are never requested are never generated.

Anyway, interesting discussion and for sure shipping less html would be a nice optimization.

@samuelgoff
Copy link
Author

But here is my confusion:

Does next.js actually generate all of these images as soon as a single one is requested, or does it only generate the srcSet needed when requested. I suspect it's the latter, in which case your statement of:

Each of these files takes time, processing & storage resources to generate and deploy. Eliminating them will deploy faster, require less processing, and require a smaller storage footprint.

probably isn't true, since srcSets that are never requested are never generated.

This is a build-time generated set of images, which is why they need to be statically imported. There may be some lazy generation in dev mode, but whenever you do a production build (e.g. deploy), this is definitely doing a dependency analysis & deciding which images & sizes to generate, and then generating them all at that time, so they can be included with the deployment.

Think about it: the build process generates a set of files that can be uploaded to a number of different JAMstack hosting providers; that set of files MUST include the images, because there is no opportunity to access the source repo after the build process (& upload) is complete.

Vercel's Edge Image Optimization feature is a completely different animal, and that is doing optimizations on-demand, then caches in CDN for up to 30 days without being accessed before being expired. If it's accessed within a shorter timeframe, then it is retained in cache.

@MartinDavi
Copy link

Ah good point, I am talking about Edge Image Optimization (or equivalent, we're self hosting). In either case, having srcSets that will never be applicable is a waste, for sure.

@samuelgoff
Copy link
Author

I don't know how Edge Image Optimization would interact with self-hosted. Are these dynamic images? If so, I would imagine the build-time generation of srcSets is irrelevant. That said, I imagine you're going to need some other service in place to accomplish what Vercel offers with their hosting, as image optimization isn't really practical on serverless or edge workers. This is one of the situations where serverful is vastly more efficient (Amazon recently discovered that moving Prime video encoding away from serverless to serverful reduced their costs by 90%).

@MartinDavi
Copy link

The image urls come through as part of the API we query in getStaticProps for page data. HTML is generated with the srcSets.

The images are not generated though at this point, next.js does not generate images that aren't statically referenced in code until request time.

Because we're self hosting, we install the sharp package on the server that is running node and the next.js server, and any requests from the front end to /_next/image?url=whatever hits the node server which generates the image requested.

Not sure if Vercel's version of this is doing something different, but we're using the out-of-the-box next/image component to accomplish this.

@samuelgoff
Copy link
Author

Interesting. I didn't realize this was supported in self-hosted environments. Are the images CDN'd as a result? If so, how long are they retained for?

@MartinDavi
Copy link

The docs have a good section on caching behaviour and our CDN respects whatever we set for the minimumCacheTTL.

@monolithed
Copy link

monolithed commented Aug 23, 2023

Has anyone managed to find a way to manually add the srcset attribute? Currently, it's generated randomly, and it's not clear who this component is even intended for.

@cbratschi
Copy link

@monolithed I ended up writing my own img component. Just ignore the TypeScript warning that next/image should be used.

@monolithed
Copy link

monolithed commented Aug 23, 2023

@monolithed I ended up writing my own img component. Just ignore the TypeScript warning that next/image should be used.

@cbratschi, could you provide an example where you were able to get it working? I tried passing the srcset attribute, but next/image ignored it.
Does your implementation cover all these aspects: automatic generation of image blur, lazy loading on scrolling, background loading of larger images, and this feature with different screen sizes? Creating a custom component seems to involve factoring in quite a number of such variables...

@cbratschi
Copy link

@monolithed This is a company project and I cannot share any source code. We ported our current logic from our web platform to Next.js. Yes, we use a lot of custom logic including direct blurhash support and lazy loading.

Basically start with <img ... /> and add the features you need. You can also have a look at the Next.js source code.

@psiho
Copy link

psiho commented Aug 27, 2023

I'm also surprised with this behavior, but it seems (by the silence of Nextjs team) it is by design and not a bug. My custom Avatar componet, which uses images of max size 128x128px, now produces several completely unnecessary srcset items, larges one being 1080x108px.

Docs say:

If the sizes property includes sizes such as 50vw, which represent a percentage of the viewport width, then the srcset is trimmed to not include any values which are too small to ever be necessary.

... so it seems they trim items that are too small. I just don't understand why they also don't trim items that are too large.

At the end, for small(er) images, I reverted back to basic IMG tag and will extend it as needed.

@monolithed
Copy link

monolithed commented Aug 28, 2023

@psiho, you're absolutely right. Typically, image sizes are tailored to match various screen dimensions, and distorting or arbitrarily altering them is unacceptable. I'd go even further to say that sometimes images meant for smaller screens end up looking wider than those intended for larger ones. There can't be a one-size-fits-all solution in this matter. No one except the users of this component can determine what the correct image sizes should be.

The component's authors suggest using image.imageSizes to expand the range of universal sizes, but these sizes are detached from reality. In real projects, each image has its own dimensions! Essentially, this component disregards the specifications and forces UI/UX-designers to prepare layouts based on the dimensions of the framework.

1400px

Screenshot 2023-08-23 at 15 46 58

800px

Screenshot 2023-08-23 at 15 46 27

450px

Screenshot 2023-08-23 at 15 46 41

If you pay attention, you'll notice that for a medium-sized screen (800px), a larger image is being used compared to a larger screen (1400px). And this is a common practice on many websites.

Even Lightroom warns when the original image is smaller than the requested dimensions:

Screenshot 2023-08-22 at 21 14 27 Screenshot 2023-08-22 at 21 16 07

@morganfeeney
Copy link
Contributor

I've recently come across this issue and it completely baffled me at first. The way <Image/> works is like an "either, or" solution, and doesn't fit with how native responsive images work.

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.

I wrote about this issue in a recent post: https://morganfeeney.com/posts/sizes-attribute-nextjs-image

@karlhorky
Copy link
Contributor

Wonder if sizes="auto" will help with this problem in future (once more browsers support it):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template. Image (next/image) Related to Next.js Image Optimization.
Projects
None yet
Development

No branches or pull requests