Skip to content

The sizes Hint Lies Once a CSS Max-Width Clamps in Next.js

Adityo Guni Waluyo

A CSS max-width clamp ships, but the vw sizes hint keeps promising a bigger slot. Make the contract honest with pixel values so srcset picks right.

TL;DR

Clamping an oversized hero image with CSS max-width looks fixed visually, but the browser still downloads a huge file. The culprit was a stale sizes attribute promising 40vw, so the browser picked a 576px srcset candidate for a 330px box. Honest pixel values in sizes, matched to the breakpoint clamp, keep downloads lean.

The first deploy after the homepage hero visual became an eagle mascot, I opened the Network tab in DevTools with a nagging feeling. The downloaded image file seemed oversized for something that renders about a third of a column wide. On screen, the mascot also looked off-balance against the text hierarchy around it.

My first guess was simple: shrink it with CSS. I swapped the className of the <Image /> component for a max-w-[220px] sm:max-w-[280px] lg:max-w-[330px] chain. Visually the result was right and proportional. I thought the job was done and nearly committed on the spot.

The Network tab disagreed. The browser was still downloading a high-resolution candidate, sized for a slot of roughly 576px, while the render box was now only 330px. The liar wasn't the CSS — it was sizes="(min-width: 1024px) 40vw, 70vw", inherited from the old component. The visual clamp shipped, but the size hint handed to the browser never made it into the update.

The false promise of the sizes hint

The browser reads sizes as a promise about the image's render slot width, then picks the srcset candidate that best fits that slot. At a 1440px viewport, 40vw means a 576px slot — and that is where the candidate choice gets computed, not from the 330px box the CSS clamp produces. The downloaded image ends up bigger than the render actually needs [2].

This slips past the radar easily because everything on screen looks normal; the waste is purely on the download side. In Next.js, the srcset shape itself also depends on sizes: without the attribute, what gets generated is a limited (1x, 2x) set for fixed-size images; with it, a full width-descriptor srcset appears (640w, 750w, and so on) [2]. web.dev explains the browser side: with width descriptors plus sizes, the browser computes the effective pixel density automatically and picks the best candidate for the promised slot [3]. As long as the promise says 40vw while reality is 330px, all that tidy math serves the wrong number.

The fix: pixel values inside sizes

The fix isn't to keep guessing in vw — it's to be honest about the render box. Pixel values are fully legal inside sizes [2], and they describe my clamp exactly. The before version:

sizes="(min-width: 1024px) 40vw, 70vw"
className="h-auto w-auto max-w-full"

The after version uses a pixel slot in sync with the clamp chain:

sizes="(min-width: 1024px) 330px, 60vw"
className="h-auto w-full max-w-[220px] sm:max-w-[280px] lg:max-w-[330px]"

The width and height props are widely misread as on-screen size setters too. All they store is the intrinsic size, used for the aspect ratio and for reserving layout space so nothing jumps while the image loads [1]. The Next.js docs say it plainly: the rendered size is CSS's job [2]. And when the sizes attribute is missing entirely, the browser assumes the image spans the full viewport width and downloads more than necessary [2] — clearly the wrong assumption for a hero component locked behind a maximum.

A habit worth keeping

I now prefer absolute pixel values in sizes whenever a component has a fixed size ceiling at a breakpoint. No more vw arithmetic that breaks the moment a clamp changes; the hint stays predictable and easy to debug. When a request looks wrong now, there is exactly one attribute to blame, and it's one I wrote myself. If a layout genuinely scales with the viewport, vw earns its place back.

A quietly broken sizes–srcset contract is expensive, and users on metered connections are the ones who pay for it. So checking the Network tab after touching image config is now a mandatory step before I call a visual change done.

Sources

Related articles