Skip to content

next/image: fixed-size images emit w-descriptor srcSet without sizes (Next.js emits 1x/2x) #1966

@Divkix

Description

@Divkix

Problem

For a fixed-size with a width and no sizes, Next.js emits an x-descriptor srcSet (e.g. ... 1x, ... 2x). vinext instead emits a w-descriptor srcSet with many device-size entries and no sizes attribute. Per the HTML spec, a w-descriptor srcSet with no sizes attribute makes the browser assume sizes=100vw, so it selects an image sized to the full viewport width rather than the element width. On high-DPR / large-viewport displays this fetches a much larger image than needed (e.g. a 1000px image fetched at 1920w), wasting bandwidth and hurting LCP — the exact opposite of what Next.js's x-descriptor path achieves. The vinext unit tests (tests/image-component.test.ts:267-284) bake in the divergent behavior.

Evidence

  • packages/vinext/src/shims/image.tsx:301 — generateSrcSet always builds a w-descriptor srcSet from device sizes filtered by <= originalWidth*2, regardless of whether a sizes prop was supplied.
  • packages/vinext/src/shims/image.tsx:604 — For a non-fill local image with a width and no sizes, the component calls generateSrcSet (w-descriptors) while leaving the sizes attribute undefined (line 634 sets imageSizes to sizes only).
  • packages/vinext/src/shims/image.tsx:744 — getImageProps has the same generateSrcSet path for fixed images, so the deviation also affects picture/background-image users.
  • .nextjs-ref/packages/next/src/shared/lib/get-img-props.ts:186 — Next.js getWidths: when no sizes prop is given but a numeric width is, it returns kind: 'x' and widths [width, width*2], i.e. a 1x/2x x-descriptor srcSet — NOT w-descriptors.

Next.js behavior (Next.js handles this correctly — vinext-only bug)

I read Next.js getWidths in get-img-props.ts and the srcSet generation that consumes it, plus the unit test that exercises the exact triggering scenario. For a fixed-size Image with a numeric width and no sizes prop, Next.js takes the typeof width === 'number' branch (line 190-205), returning kind: 'x' with widths [width, width*2], then emits an x-descriptor srcSet (... 1x, ... 2x) at lines 265-272 and deliberately leaves sizes undefined (line 264 only forces 100vw when kind === 'w'). The unit test at lines 46-67 codifies this: width 100/no sizes yields w=128 1x, w=256 2x and no sizes attribute. vinext's generateSrcSet (image.tsx:301-306, used at :604-606 and getImgProps at :744) instead always builds a w-descriptor srcSet from device sizes filtered by <= width*2, and leaves sizes undefined (image.tsx:634) — which per the HTML spec makes a w-descriptor srcSet default to sizes=100vw, causing the over-fetch. So the defect is vinext-only; Next.js handles this scenario correctly.

Citations: .nextjs-ref/packages/next/src/shared/lib/get-img-props.ts:186-205 (getWidths: when no sizes and numeric width, returns { widths: [width, width*2], kind: 'x' }); :260-272 (srcSet built with kind === 'w' ? w : i + 1 + ${kind}, so kind='x' yields ... 1x, ... 2x); :264 (sizes: !sizes && kind === 'w' ? '100vw' : sizes — with kind='x', sizes stays undefined, NOT forced to 100vw); test .nextjs-ref/test/unit/next-image-get-img-props.test.ts:46-67 asserts exactly this scenario (width:100, height:200, no sizes) producing ... w=128 1x, ... w=256 2x srcSet with no sizes prop emitted.

Suggested fix

Mirror Next.js getWidths: when no sizes prop is present and a numeric width is, generate an x-descriptor srcSet from [width, width*2] mapped to the smallest device size >= each; only use w-descriptors (and default sizes when needed) when a sizes prop is provided or the image is fill. Update image-component.test.ts expectations accordingly.

Test plan

Port .nextjs-ref/test/unit/next-image-get-img-props.test.ts:46-67 (width, no sizes → 1x/2x, no sizes attr).

Related / notes

tests/image-component.test.ts:267-284 currently bakes in the divergent behavior and must be updated.


Found via a deep source audit of main @ fd10233 (2026-06-12). Behavior parity-checked against Next.js v16.3.0-canary.7 source; citations above reference packages/next/src/... in the Next.js repo. Screened against all open issues/PRs as of 2026-06-12 to avoid duplicating tracked work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions