next/image Without Dimensions — Cumulative Layout Shift Sent SEO Rankings to Page 3
How missing width/height on Next.js images causes CLS that destroys Core Web Vitals.
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓React fundamentals
- ✓Next.js 13+ project
- ✓Basic understanding of Core Web Vitals (CLS, LCP)
- Missing width/height on next/image causes Cumulative Layout Shift (CLS) that tanks Core Web Vitals and drops SEO rankings
- next/image requires explicit width/height or fill prop — without them, the browser reserves 0x0 space until the image loads
- next/font with
display: 'swap'also causes layout shift if you don't preload or usedisplay: 'optional' - Use
sizesprop and responsive breakpoints to serve correctly sized images for each viewport - Pair with
for hero images and critical fonts to eliminate layout shift entirely
Imagine building a bookshelf but not marking where each book goes — when the books arrive, you have to rearrange everything, knocking stuff over. That's CLS. Images without dimensions are unmarked boxes; when they load, the whole page rearranges. next/image and next/font are the tape measure and blueprint that tell the browser exactly where everything sits before it arrives.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
You deployed your Next.js 16 marketing site. Lighthouse was green in development. Two weeks later, your SEO lead emails: "We dropped from position 3 to page 3 for our main keyword." You check Search Console — Core Web Vitals are red. Cumulative Layout Shift is 0.35. The threshold for 'poor' is 0.25. You're not just failing — you're failing spectacularly.
The culprit? Hero images without width and height. Your next/image components were happy in dev mode because Next.js can optimize locally, but in production, the browser has no idea how tall those images are until they download. Every pageload, text jumps down by 400px when the hero loads. Google sees that as a terrible user experience and drops your rankings.
This isn't just about images. next/font with display: 'swap' causes the exact same problem — when a custom font loads, the text re-renders in a different size, shifting everything below it. Two features designed to improve performance are actively sabotaging your SEO because of one missing prop.
By the end of this guide, you'll know exactly how to configure next/image and next/font for zero CLS in production. No guesswork. No Lighthouse green in dev but red in prod. Real configuration that survives a Vercel deployment.
How CLS Is Calculated and Why It Matters for SEO
Cumulative Layout Shift measures the sum of all unexpected layout shifts during a page's lifecycle. Google calculates it as impact fraction * distance fraction. An impact fraction of 0.5 (half the viewport shifted) times a distance of 0.5 (moved 50% of the viewport height) gives you 0.25 CLS — the threshold for 'needs improvement'.
Here's the kicker: Google's CLS thresholds are 75th percentile of all visits. If even a quarter of your mobile users see a layout shift (because they're on 3G or have a full cache), you fail. This is why missing image dimensions are so catastrophic — they affect users with slower connections disproportionately, and those users determine your CrUX score.
next/image: The Three Configurations for Zero CLS
next/image in Next.js 16 has three modes. Known dimensions: pass width and height directly. The component sets inline width and height attributes on the <img>, telling the browser to reserve space before the image loads. This is zero CLS by design.
Full-bleed / unknown dimensions: use the fill prop. The image becomes position: absolute and fills its parent. The parent MUST have position: relative, absolute, or fixed, plus an explicit or percentage-based height. Without a defined height, fill images collapse to 0px.
Responsive grids: use fill with objectFit and sizes. The sizes prop tells the browser which image to download at each breakpoint, preventing oversized downloads that cause slower load times.
The sizes Prop: Stop Downloading 4000px Images on Mobile
The sizes prop is the most misunderstood Image optimization. Without it, next/image defaults to 100vw — it downloads the largest source image available. On a 4000px image, that's a 2MB download on a phone that displays 375px. The image still renders at the right size, but the download is 10x larger than needed.
sizes tells the browser which image source to pick from the srcset that next/image generates. The syntax matches the HTML sizes attribute: (max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw means "on screens under 640px, use the image sized for full viewport width; between 640-1024px, use half viewport; above that, use one-third."
This doesn't affect CLS directly, but it affects Largest Contentful Paint (LCP) — your hero image can't paint until it finishes downloading. Oversized images on slow connections delay LCP, which is a separate Core Web Vital failure.
next/font: The Hidden Source of Layout Shift
next/font is a wrapper around the CSS @font-face declaration with automatic font subsetting and self-hosting. But the default display strategy controls what happens before the font loads — and the default matters.
In next/font, if you don't specify display, it defaults to 'swap'. With swap, the browser renders text in the fallback font immediately and swaps to the custom font once it loads. If the custom font has different metrics (most do), the text re-renders at a different size, shifting everything below it by a few pixels.
For body fonts, use display: 'optional'. This gives the browser 100ms to load the font. If it doesn't load in time, the fallback is used for the entire session. The text never shifts because the fallback stays in place. For heading fonts where the brand font is critical, use display: 'swap' only if you've tuned the adjustFontFallback option to match metrics.
Font Metric Matching: Make Swap Look Identical to the Fallback
The adjustFontFallback option in next/font automatically adjusts the fallback font's size and line height to match your custom font's metrics. When this is enabled, the swap from fallback to custom font is invisible — the text occupies exactly the same space, so there's zero layout shift.
next/font uses a size-adjust CSS property on the @font-face fallback declaration. It calculates the ratio between your font's ascent, descent, line gap, and unitsPerEm, then adjusts the fallback to match. For Inter, the size adjustment is approximately 90% — the fallback renders slightly smaller to match Inter's compact vertical metrics.
Enable this for all fonts that use display: 'swap'. It's on by default in next/font, but only if you don't override it. If you're using a third-party font loading library or manual @font-face, you need to calculate and apply size-adjust yourself.
Preloading Critical Images and Fonts
Even with correct dimensions, your hero image still has to download. Without preloading, the browser discovers the image only when it parses the HTML for that component — which can be several round trips into the page load. For hero images (LCP candidates), add the priority prop to next/image. This adds a <link rel="preload"> tag for that image in the <head>, telling the browser to start downloading immediately.
For fonts, next/font automatically adds preload links for the font files. You can confirm this by checking the <head> of your rendered page. The preload tells the browser to fetch the font file during the initial HTML download, before CSS parsing begins.
One caveat: only preload the font variants you actually use. If you import Inter with weight: ['400', '700'], both weights get preloaded. That's two extra font files on every page. For sites with many pages, this can add unnecessary network requests. Consider loading secondary font weights lazily using CSS font-display swap without preload.
Static Import vs Remote Images: Different Configs
When you import an image statically (import hero from './hero.jpg'), next/image automatically extracts width and height from the file. You don't need explicit dimensions — the component knows them from the import. This is the safest approach for zero CLS.
For remote images (CMS, S3, external URLs), you MUST provide either explicit width/height, the fill prop, or configure remotePatterns in next.config.js. Without remotePatterns, remote images fall back to unoptimized <img> tags, which don't auto-reserve space.
remotePatterns also controls image optimization. Each pattern specifies a protocol, hostname, port, and optional pathname. Images that don't match any pattern are served unoptimized — no resizing, no WebP conversion, no srcset generation. Without a srcset, the browser defaults to the original file, which could be 4000px wide on a mobile screen.
The Image Component's quality and formats for Production
next/image defaults to 75% quality for optimization. That's a reasonable default, but for hero images, you might want 80-85%. For thumbnails, 50-60% is indistinguishable and saves 40%+ bandwidth. Set quality on individual images or globally in next.config.ts with images.quality.
Format-wise, next/image automatically serves WebP to browsers that support it. For even better compression, enable AVIF by adding formats: ['image/avif', 'image/webp'] to your next.config. AVIF is ~50% smaller than WebP at equivalent quality. Safari supports AVIF as of macOS Sonoma, so adoption is near-universal.
Combine this with proper sizing and you reduce image payload by 70-80%. A 2MB hero JPEG becomes a 120KB WebP or 60KB AVIF. That's four fewer seconds of download time on a 3G connection — directly improving LCP.
Font Subsetting: Ship Only the Glyphs You Use
next/font automatically subsets fonts based on the subsets option. If you use subsets: ['latin'], it strips all non-Latin glyphs from the font file. This reduces Inter from a ~1.5MB font file with all subsets to ~100KB for just Latin characters.
For sites that support multiple languages (e.g., Latin + Cyrillic), add both subsets: subsets: ['latin', 'cyrillic']. next/font generates separate font files for each subset and loads only the relevant ones per page by inspecting the Accept-Language header or the lang attribute on <html>.
This is critical for performance: if your site supports 10 languages but only serves one per page, you don't need to download all 10 language fonts. Each extra subset is another 50-100KB per font weight.
Font Loading Strategies: swap vs optional vs block
The display option in next/font maps directly to the CSS font-display property. There are three main strategies:
block: The browser hides text for up to 3 seconds while the font loads (FOIT — Flash of Invisible Text). Once loaded, text appears in the custom font. This avoids CLS but creates a blank page for up to 3 seconds. Bad for LCP.
swap: The browser renders text in the fallback font immediately, then swaps to the custom font when it loads (FOUT — Flash of Unstyled Text). This causes CLS if metrics differ. Good for headings where brand accuracy matters.
optional: The browser gives the font 100ms to load. If it doesn't, the fallback is used for the entire session. No swap ever happens after that window. Zero CLS. Best for body text where the custom font is nice to have but not critical.
Monitoring CLS in Production with Real User Monitoring
Lighthouse is a lab tool — it tests your page on a simulated device with a simulated connection. Real User Monitoring (RUM) measures what actual users experience. Google's Chrome User Experience Report (CrUX) is free RUM data from Chrome users, accessible via PageSpeed Insights or the CrUX API.
To get reliable CLS data, use the web-vitals library in your Next.js app. Add an onReport handler to reportWebVitals in your app/layout.tsx or pages/_app.tsx. Send the data to your analytics (GA4, Datadog, Sentry). Monitor the 75th percentile CLS value daily — that's what Google uses for rankings.
Set up alerts: if CLS at the 75th percentile exceeds 0.1 for any page in the last 7 days, page the team. Fighting CLS regression after a release is much harder than catching it in the first 24 hours.
A Complete Production Configuration for Zero CLS
Here's a battle-tested setup. The layout imports Inter with display: 'optional' for body text and a separate heading font with display: 'swap' and adjustFontFallback. Hero images use priority with explicit dimensions. Gallery images use fill with sizes prop. Fonts are subset to Latin only unless you support multiple languages.
The next.config.ts enables AVIF and WebP, sets a sensible quality baseline, and configures remotePatterns for your CMS images. You also enable the deviceSizes and imageSizes to control which sizes next/image generates in the srcset.
Test with web-vitals instrumentation in production. Deploy, monitor the 75th percentile CLS for 7 days, and sleep well knowing your SEO rankings aren't at risk from a missing width prop.
The Trade-off: Quality vs. Performance in Images
There's no free lunch. Higher quality means larger files, which means slower LCP. The question is where you draw the line. For hero images, 85% quality is typically visually lossless. For thumbnails, 50% quality looks fine at 150px wide. For product photos on an e-commerce site, 80% quality is the sweet spot — anything above is indistinguishable to most users.
next/image's quality prop defaults to 75, which is conservative. I run 85 for hero images and 60 for thumbnails. Test with your own images: export a JPEG at 75, 80, 85, and 90, compare at 2x zoom. If you can't tell the difference, use the lower quality. The 5% difference in visual quality doesn't justify the 30% increase in file size.
For AVIF, quality is not directly comparable to JPEG. An AVIF at quality 50 is roughly equivalent to JPEG quality 80 in visual quality. Start with quality: 60 for AVIF and adjust. The default 75 for AVIF produces files that are larger than necessary.
Production Incident: 35,000 Users Experienced Layout Shift on Every Page Load
layout='responsive' without width and height.fill prop with a parent container). Without them, the browser renders the image at 0x0 until it loads, then inserts the image at its natural dimensions, shifting all subsequent content.width={1200} and height={630} to all hero images. For full-bleed backgrounds, switched to fill with className="object-cover". Added sizes prop to serve correct sizes: sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw".- Never trust dev-mode Lighthouse scores — they often miss production-specific CLS from images
- Always add explicit width and height to next/image, even for responsive layouts
- Use the
fillprop when dimensions are unknown but always wrap in a positioned parent with known aspect ratio - Audit CLS with WebPageTest.org, not just Lighthouse — it catches mobile-specific layout shifts Lighthouse misses
display: 'swap', text will reflow when the font loads.next build && next start and test locally.sizes prop to serve smaller images on mobile.grep -r '<Image' src/ | grep -v 'width=' | grep -v 'fill'grep -r 'next/font' src/ | grep 'display.*swap'| File | Command / Code | Purpose |
|---|---|---|
| components | | next/image | |
| components | The sizes Prop | |
| app | const inter = Inter({ subsets: ['latin'] }) | next/font |
| styles | /* Manual size-adjust if not using next/font */ | Font Metric Matching |
| app | | Preloading Critical Images and Fonts | |
| next.config.ts | Static Import vs Remote Images | |
| next.config.ts | const nextConfig = { | The Image Component's quality and formats for Production |
| app | export function reportWebVitals(metric: NextWebVitalsMetric) { | Monitoring CLS in Production with Real User Monitoring |
| next.config.ts | const nextConfig: NextConfig = { | A Complete Production Configuration for Zero CLS |
Key takeaways
Interview Questions on This Topic
How does next/image prevent Cumulative Layout Shift, and what happens when dimensions are missing?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's Next.js. Mark it forged?
7 min read · try the examples if you haven't