Why Your Next.js Images Are Still Slow (Even With next/image)
The three next/image mistakes that quietly wreck your LCP score: missing priority on hero banners, lazy sizes attributes, and no cache directory on self-hosted deploys.
Why Your Next.js Images Are Still Slow (Even With next/image)
Most Next.js projects already use next/image. Almost none of them are actually getting the LCP numbers it's supposed to deliver.
That gap is usually not a next/image problem. It's three specific misconfigurations that are easy to ship without noticing, because the component still renders a perfectly fine-looking image either way. The page just quietly fails Core Web Vitals in the background.
The hero image is loading like it's not the hero
If the largest image above the fold doesn't have priority, the browser has no reason to treat it as urgent. It gets discovered the same way as any other image on the page: after the HTML parses, sometimes after hydration kicks in. For a hero banner, that delay usually is your LCP.
// components/HeroBanner.tsx
import Image from 'next/image';
export function HeroBanner({ src, alt }: { src: string; alt: string }) {
return (
<div className="relative h-[480px] w-full overflow-hidden rounded-2xl">
<Image
src={src}
alt={alt}
fill
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
quality={85}
className="object-cover"
/>
</div>
);
}
Setting priority makes Next.js emit a <link rel="preload" as="image"> tag directly in the server-rendered <head>, so the browser starts fetching before it's even finished parsing your CSS and JS bundles. On a typical hero banner this alone is worth 400ms to 1.2s off LCP. It costs nothing to add and there's rarely a reason not to use it on whatever image sits above the fold.
sizes is the one everyone gets wrong
Here's the part that trips people up even when they know priority exists: sizes isn't optional metadata, it's the instruction that decides which image width actually gets downloaded.
Leave it off fill images and Next.js assumes 100vw. On a 3-column product grid, that means a phone on a 390px screen ends up requesting the same 1920px desktop asset a full-width layout would need, then the browser (or your image pipeline) does the resizing math wrong at the CDN layer. You end up shipping a ~320KB image to display something that should have cost ~35KB.
// A 3-column desktop grid that drops to 2 columns on tablet, 1 on mobile
<Image
src={product.imageUrl}
alt={product.name}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover"
/>
Getting sizes right, matched to your actual breakpoints, is where you recover most of the bandwidth: up to 75% on mobile, by some measurements. It also shaves roughly 250ms off mobile LCP, since the browser isn't waiting on a payload ten times bigger than what it needs to paint.
next.config.js still needs tuning, even with defaults
Two settings in next.config.js do a disproportionate amount of the work:
/** @type {import('next').NextConfig} */
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 2592000, // 1 month at the edge
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.yourdomain.com', pathname: '/uploads/**' },
],
},
};
AVIF first, WebP as fallback, gets you roughly another 20% off file size over WebP alone, worth about 150ms of LCP on top of everything above. That part's a one-line change and there's no real downside.
minimumCacheTTL matters more than it looks like it should if you're self-hosting instead of deploying to Vercel. Vercel's edge network caches transformed images for you automatically. A self-hosted deployment on your own VPS or container doesn't get that for free — without a persistent cache directory, sharp reprocesses the same image transformation on every single request. I've seen this exact thing tank response times on a client project where the "optimized" images were actually slower than serving the originals, because the server was doing full resize-and-recompress work on every page load instead of once.
What actually moves the needle
| Fix | LCP impact | Bandwidth saved | CLS impact |
|---|---|---|---|
priority on the hero image |
-400ms to -1.2s | 0% | 0.00 |
Accurate sizes breakpoints |
-250ms (mobile) | up to 75% | 0.00 |
formats: ['image/avif'] |
-150ms | +20% on top of WebP | 0.00 |
Explicit width/height or fill |
~0ms | 0% | Eliminates shift (CLS < 0.01) |
If you only fix one thing today, fix sizes. It's the setting most people skip because the image still looks right without it, and it's the one costing the most real bandwidth on mobile. priority is the easy win everyone already knows about. The cache directory issue is the one that only bites you once you're off Vercel, and by then it's usually already been quietly inflating your server bill for a while.
Accelerate your Frontend & Performance Modernization Roadmap
Need custom architecture auditing, automated OpenAPI contract generation, or zero-downtime microservice migration guidance for your engineering team?
Frequently Asked Questions
Do I still need to write a sizes attribute if I'm using fill on a next/image?
Yes. fill controls how the image fits its container, but sizes is what tells the browser which of the generated srcset widths to actually request. Skip it and the browser defaults to 100vw, which on a responsive grid means it downloads way more image than the layout ever shows.
Does self-hosting Next.js need any extra image caching config?
It needs a persistent cache directory for the image optimizer, or every request re-runs the sharp transformation from scratch. Vercel handles this automatically at the edge; a self-hosted deployment on a VPS or container has to be told where to keep the processed files, otherwise you're paying the CPU cost of resizing the same image over and over.
Subscribe to RenovateAPI
Get weekly architectural guides, API refactoring strategies, and technical SEO updates delivered directly to your inbox.
Discussion (2)
Extremely helpful breakdown of the Strangler Fig pattern! We're currently refactoring a legacy Java monolith at work and the OpenAPI gateway routing tips saved us weeks of experimentation.
The schema JSON-LD and FAQ block structure really helps with indexing. Great technical detail on entity mentions too.
Suggested Related Articles
Your Next.js Proxy Is Probably Adding 300ms to Every Request
How to build a fast, edge-compatible proxy.ts in Next.js App Router — stateless JWT auth with jose, HTTP-based Redis rate limiting, and a matcher config that stops running on every image request.
Stop Overusing revalidatePath — Fix Stale Next.js Caches with Tags Instead
revalidatePath() purges entire route trees and wastes database calls. Here's how hierarchical revalidateTag() fixes stale ISR reads without the origin load.
Fixing React 19 Hydration Mismatches in Next.js App Router
The three most common causes of hydration mismatch errors in React 19 and Next.js App Router — localStorage reads, timezone-dependent dates, and browser extensions — with working fixes for each.