How to Fix Slow LCP with the Next.js Image Component
This guide is part of Next.js Performance, within Framework Performance. The Next.js Image component (next/image) generates responsive srcsets, serves modern formats through the built-in optimiser or a custom loader, sets dimensions to prevent layout shifts, and lazy-loads images by default. That last default is the most common cause of slow LCP in Next.js apps: unless the hero image is marked with priority (or fetchPriority="high" and loading="eager" in newer versions), it is lazy-loaded and discovered late.
Other configuration problems compound it: a missing or wrong sizes prop makes the browser download images far larger than displayed; fill images without a sizes default to 100vw; the optimiser transforms images on first request, which can take hundreds of milliseconds on a cache miss; and high default quality settings waste bytes.
Rapid Diagnosis
- Inspect the LCP image's HTML:
loading="lazy"meanspriorityis missing. - Check the downloaded width against the displayed width in DevTools (Network panel image preview shows intrinsic size).
- Check
/_next/imageresponse times on first request versus cached. - Check for a preload link in the head for the LCP image.
Root Cause Analysis
1. Lazy loading by default. The hero waits until layout determines it is in view.
2. Missing or wrong sizes. The browser picks large candidates.
3. Cold optimiser cache. First requests transform images synchronously.
4. Client-rendered hero. The image URL is known only after JavaScript runs.
Step-by-Step Resolution
1. Mark the LCP image as priority
import Image from 'next/image';
<Image src={product.heroUrl} alt={product.name} width={1200} height={900}
priority sizes="(max-width: 768px) 100vw, 50vw" quality={70} />
// trade-off: priority preloads the image with high fetch priority; mark only the
// one or two images likely to be LCP, or they compete with each other.
2. Set sizes to match the layout
sizes should describe the image's rendered width at each breakpoint. For fill images, always set sizes.
3. Use a CDN-backed loader or cache the optimiser
For high-traffic sites, configure a custom loader that points at an image CDN, or ensure the built-in optimiser's output is cached at the CDN with a long minimumCacheTTL.
// next.config.js — custom loader for an image CDN.
module.exports = { images: { loader: 'custom', loaderFile: './image-loader.js', formats: ['image/avif', 'image/webp'] } };
// image-loader.js
export default function loader({ src, width, quality }) {
return `https://img.example-cdn.com${src}?w=${width}&q=${quality || 70}&auto=format`;
}
4. Render the hero on the server
Make sure the hero image's URL is in the server-rendered HTML (Server Components or SSR), not determined after client-side data fetching.
Verification
In DevTools, the LCP image should have fetchpriority="high", no loading="lazy", and a preload link in the head. The downloaded image width should be close to displayed width × DPR. Image responses should come from cache (check x-nextjs-cache or CDN headers). In the field, LCP for the template should improve, particularly the resource load delay and load duration parts.
Worked Example: A Real Estate Listing Site
A real estate site's listing pages used next/image with fill for the main photo, no priority and no sizes. The browser lazy-loaded a 3840px image for a 600px slot on mobile. LCP p75 was 3.4 seconds. Adding priority and sizes="(max-width: 768px) 100vw, 60vw" cut the image from 680KB to 110KB on mobile and removed the lazy-load delay. Moving optimisation to an image CDN via a custom loader removed 300–700ms transform delays on cache misses for new listings. LCP p75 fell to 1.8 seconds.
Priority, fetchPriority and Preloading
In Next.js, priority does two things: it disables lazy loading and adds a preload link with high fetch priority in the document head. Newer Next.js versions also expose fetchPriority and loading directly, and may change how priority works; check the documentation for your version. Either way, the goal is the same: the LCP image must be discoverable in the initial HTML, requested early, and not deprioritised. Avoid marking many images as priority — they compete for bandwidth, which can make the real LCP image slower.
Placeholder Blur and LCP
placeholder="blur" shows a blurred low-resolution version while the full image loads. It improves perceived loading but does not count as the LCP: LCP fires when the full image renders. The inline blur data adds a little HTML weight. It is a good choice for below-the-fold and gallery images; for the hero, focus on making the full image arrive fast.
Finding the LCP Image per Template
The LCP element varies by template and viewport: the product photo on product pages, a hero banner on the homepage, sometimes a headline instead of an image on mobile. Check each key template at mobile and desktop widths using the Performance panel's LCP marker or the web-vitals attribution build, which reports the LCP element's selector and URL in the field. Mark only that element as priority, and revisit after redesigns. When the LCP image differs by breakpoint, use getImageProps with a picture element so each breakpoint's image is the one preloaded.
Common Mistakes
- No priority on the hero. The single most common Next.js LCP problem.
- fill without sizes. Defaults to full viewport width.
- Many priority images. Competing high-priority downloads.
- Uncached optimisation. Slow transforms on every cache miss.
Edge Cases
Static imports. Statically imported images get automatic dimensions and blur placeholders; still set priority and sizes.
Remote images. Configure remotePatterns to allow optimisation of remote sources.
Art direction. Use the getImageProps helper with a picture element for different crops per breakpoint.
Unoptimized images. unoptimized skips the optimiser; ensure the source is already sized and in modern formats.
FAQ
Why is my Next.js hero image lazy-loaded?
next/image lazy-loads by default. Add priority (or the equivalent loading and fetch priority props in your version) to the LCP image.
What should sizes be?
A media-query list describing the rendered width at each breakpoint, such as (max-width: 768px) 100vw, 50vw.
Is the built-in optimiser fast?
Cached responses are fast; first requests transform images and can be slow. Cache at the CDN or use an image CDN loader.
Should I enable AVIF?
Yes, with formats: ['image/avif', 'image/webp']. AVIF encoding is slower on cache misses, so caching matters more.
Does next/image prevent CLS?
Yes, when dimensions are set (width and height, or fill within a sized container).
How many images should have priority?
Usually one — the likely LCP image. At most a couple when the LCP element differs by viewport.
Can I use next/image for background images?
Use fill with object-fit: cover in a positioned container instead of CSS backgrounds, so the image is discoverable and optimised.
What quality setting should I use?
Around 60–75 is a good range for photos with modern formats; validate visually for your content.
Does next/image work with static exports?
The default optimiser needs a server. For static exports, use a custom loader pointing at an image CDN, or pre-generate sized images.
Should I use unoptimized for SVGs?
Yes. SVGs do not benefit from raster optimisation; serve them directly.
Related
- Using fetchpriority to prioritize the LCP image — the underlying browser mechanism.
- Caching transformed images at the edge — avoiding slow transforms.
- Next.js partial prerendering — keeping the hero in the static shell.