Next.js Image Optimization: Getting CLS and LCP Right
How next/image prevents layout shift, what sizes and priority actually do, and the configuration mistakes that make images slower rather than faster.
Table of contents
- Layout space is the CLS fix
- priority is for the LCP element only
- sizes is the prop people get wrong
- Remote images need explicit hosts
- When not to use next/image
- Frequently asked questions
- Does next/image work with a static export?
- Should I use a blur placeholder?
- Why is my image still shifting layout?
- Does the optimizer cost money on Vercel?
- Related reading
- References
next/image does four things a plain <img> does not: it reserves layout space, serves modern formats, generates responsive sources, and lazy-loads by default. Three of those directly affect Core Web Vitals.
Layout space is the CLS fix#
Cumulative Layout Shift happens when an image loads and pushes content down. The cause is always the same: the browser did not know the dimensions in advance.
// width and height are REQUIRED for a non-fill image
<Image src="/hero.jpg" alt="Dashboard" width={1200} height={630} />Those numbers are an aspect ratio, not a rendered size — CSS still controls display size. What they buy is a reserved box before the bytes arrive.
For images whose size is unknown, fill with a positioned parent:
<div className="relative aspect-video">
<Image src={url} alt="" fill className="object-cover" />
</div>The parent must have a defined size or aspect ratio, or you have moved the problem rather than solved it.
priority is for the LCP element only#
Every next/image is lazy-loaded unless told otherwise. That is right for everything below the fold and wrong for the hero, which is usually the Largest Contentful Paint element.
<Image src="/hero.jpg" alt="" width={1200} height={630} priority />priority disables lazy loading and adds a preload hint. Marking several images priority defeats the purpose — they compete for bandwidth and LCP gets worse, not better. One per page, at most.
sizes is the prop people get wrong#
Without sizes, a responsive image defaults to 100vw, so the browser may download a 1920px-wide file for a 300px card.
// A 3-column grid on desktop, 2 on tablet, 1 on mobile
<Image
src={url}
alt=""
width={400}
height={300}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>sizes describes the rendered width at each breakpoint. Getting it right is often the single biggest image payload win available, and it costs one line.
Remote images need explicit hosts#
// next.config.ts
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ protocol: 'https', hostname: 'images.example.com' },
],
},The allowlist is a security control, not bureaucracy: without it, your optimizer becomes an open image proxy that anyone can point at any URL and bill to your account.
AVIF before WebP in formats is worth it — typically 20-30% smaller than WebP at the same quality — at the cost of slower optimisation on first request.
When not to use next/image#
Two legitimate cases:
Data URLs generated in the browser. There is nothing to fetch or resize, and the optimizer will reject the src. Use a plain <img> with an eslint-disable and a comment saying why.
Inline SVG. An icon or logo in the header should be inline SVG: no extra request, no CLS risk, and its colours can reference CSS variables so it themes automatically.
Frequently asked questions#
Does next/image work with a static export?#
Not with the default loader, which needs a server. Set images: { unoptimized: true } or configure a custom loader pointing at a CDN.
Should I use a blur placeholder?#
For local imports, yes — Next generates it at build time for free. For remote images you must supply blurDataURL yourself, and a large one adds bytes to the HTML; keep it tiny.
Why is my image still shifting layout?#
Either it is a fill image whose parent has no dimensions, or CSS is overriding the aspect ratio (height: auto with a fixed width is the usual culprit).
Does the optimizer cost money on Vercel?#
Transformations are metered. Caching means each unique size/format is generated once, which is another reason to get sizes right rather than serve dozens of variants.
Related reading#
- Core Web Vitals: A Practical Guide
- Next.js App Router Guide
- Need to inline a small icon? Image to Base64 shows the size cost before you commit.