Next.js

Next.js Image Optimisation: sizes, priority, quality and LCP

Getting next/image right for LCP: the sizes attribute most sites get wrong, when to use priority, quality and format trade-offs, and the check that proves it.

5 min read
Next.jsPerformanceCore Web Vitals
942 words5 min read

What next/image does

The Image component from next/image renders an <img> with a srcset of resized, re-encoded versions served through the image optimisation route (or a configured loader), with width and height set to prevent layout shift, lazy loading by default, and a placeholder option. On a well-configured site it is the reason images stop being the performance problem.

On a badly configured one it serves a 1920-pixel image to a phone, lazy-loads the hero and delays LCP by two seconds. The difference is four props.

The prop that matters most: sizes

srcset lists the available widths. sizes tells the browser how wide the image will be displayed, so it can pick the right one before layout. Without sizes, the browser assumes the image is the full viewport width and picks accordingly. On a 390-pixel phone with a 3x display, that is a 1170-pixel image, for a photo in a 300-pixel card.

Every Image that is not full-width needs an honest sizes:

TSX
// A card in a three-column grid that stacks on mobile
<Image
  src={photo}
  alt="..."
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

// A hero that is always full-width
<Image src={hero} alt="..." sizes="100vw" priority />

// A fixed-size avatar
<Image src={avatar} alt="..." width={64} height={64} sizes="64px" />

Think of sizes as a description of the CSS layout in media-query terms. If the image is in a column capped at 720 pixels, (max-width: 768px) 100vw, 720px. Get this right and the bytes downloaded on mobile typically fall by two thirds.

With the fill prop (image fills a positioned parent), sizes is mandatory in spirit; without it the browser downloads the largest candidate.

priority: only for the LCP image

Lazy loading is the default and correct for everything below the fold. It is wrong for the image that is the Largest Contentful Paint element, because that image should start downloading before layout is complete, not after the browser has decided it is in view.

priority on an Image disables lazy loading, adds fetchpriority="high" and emits a <link rel="preload"> for it. Use it on exactly the image that is the LCP element on that page, usually one, sometimes none if the LCP is text. Do not put it on every image above the fold; that dilutes the priority and floods the connection.

Find the LCP element in DevTools' Performance panel, or with the web-vitals library's attribution. It is frequently not the image you assumed.

quality and format

Default quality is 75, which is right for most photographs. Going to 85 or 90 on a hero doubles the bytes for a difference most people cannot see; going to 60 on thumbnails is often free. Set quality per image where it matters and leave the default elsewhere. Next.js 16 lets you restrict allowed qualities in config, which stops the optimiser generating variants nobody asked for.

Formats: the optimiser serves WebP by default, and AVIF if enabled in next.config (images.formats: ['image/avif', 'image/webp']). AVIF is smaller and slower to encode; on a platform with an image cache the encode cost is paid once. Enable it.

Static imports versus remote URLs

A static import (import hero from './hero.jpg') gives Next.js the intrinsic dimensions at build, so width and height are automatic and a blur placeholder can be generated. Prefer it for images in the repository.

Remote images (from a CMS or CDN) need explicit width and height or fill, and the host must be allowed in images.remotePatterns. Give the CMS the job of providing dimensions; most do. If your CMS or CDN already optimises (Cloudinary, Imgix, Sanity's image pipeline), a custom loader lets Image build their URLs and skips Next.js's optimiser entirely, which is usually the right choice.

Placeholders and layout shift

Image sets width and height, so the browser reserves space and there is no layout shift when the image arrives. Do not override that with CSS that changes the aspect ratio. placeholder="blur" with a static import shows a tiny blurred version until the real one loads, which makes slow connections feel faster and costs a few hundred bytes inline.

The common mistakes, quickly

  • No sizes on a non-full-width image. The default.
  • priority missing on the hero, so it lazy-loads. LCP suffers.
  • priority on many images. Nothing is prioritised.
  • A CSS background-image as the hero. Not discoverable, no srcset. Use Image with fill and object-fit: cover.
  • Enormous source images from the CMS with no cap. The optimiser handles it but the origin fetch is slow; cap uploads at 2500 pixels or so.
  • unoptimized set globally because of a deployment quirk, and never removed.
  • Icons and logos through the optimiser. SVGs should be plain <img> or inline; the optimiser does nothing useful for them.

Verifying

Lighthouse on mobile, "Properly size images" and "Largest Contentful Paint element". The LCP element should be your priority image, the "properly size" audit should be empty, and the hero should appear in the network panel with high priority and no delay after the HTML. Then, after a month, the field LCP in Search Console.

The before-and-after on sites where sizes and priority were wrong is typically an LCP dropping from around three seconds to well under two on a throttled mobile profile, with no other change.

Where this fits

Getting Image right is a checklist item on every Next.js build, and the four props above are the whole checklist: honest sizes, priority on the LCP image only, default quality unless there is a reason, and AVIF on. The rest is finding out which image is actually the LCP element, which is the part that surprises people.

All writingHire me for this