Stop Using <img> in Next.js: Why next/image is Essential for LCP
"Why Is My Image Loading So Slow?"
I added a high-res hero image to the main page.
I dumped the designer's hero.png (3MB) straight into an <img> tag.
Run Lighthouse... Performance: 45. A glaring red warning. LCP (Largest Contentful Paint) took 4.2 seconds. Users were staring at a white screen for 4 seconds.
"It's just one image! Why did it tank my score?"
What Confused Me Initially? (The Original File Trap)
I thought "Images are supposed to be heavy." I thought I could serve the same file to mobile and desktop.
But browsers aren't smart.
Give a mobile processing a 3MB file, and it downloads all 3MB. The phone heats up decoding it.
Plus, png is terribly inefficient for photos.
The 'Aha!' Moment (Magic Converter Analogy)
I understood it as a "Universal Adapter Pipeline."
<img>: Throwing the raw file. "Here, take this 3MB rock."next/image: An intelligent server that tailor-makes clothes.- iPhone user? -> "Screen is small. Resizing to 300px."
- Chrome? -> "You support WebP? Converting format."
- Below fold? -> "Don't load yet. Wait." (Lazy)
I didn't need to manually create hero-mobile.webp.
Next.js server does it On-Demand.
The Fix: Adopting next/image
Step 1: Local Image (The Easy Way)
Importing local files lets Next.js calculate dimensions automatically.
import Image from 'next/image';
import heroImg from '../public/hero.png';
// Before ❌
// <img src="/hero.png" alt="Hero" />
// After ✅
<Image
src={heroImg}
alt="Hero"
placeholder="blur" // Blur-up effect! (Auto generated)
priority // Critical for LCP!
/>
priority tells the browser to Preload this image, skipping lazy loading. This is the key to Green LCP.
Step 2: Remote Images
For AWS S3 URLs, you must specify dimensions and allow the domain.
// next.config.js
module.exports = {
images: {
domains: ['my-bucket.s3.amazonaws.com'],
},
}
<Image
src="https://my-bucket.s3.amazonaws.com/user.jpg"
alt="User"
width={300} // Required for aspect ratio calculation
height={300}
/>
Step 3: The Secret of sizes (Crucial!)
Many people ignore sizes prop. Without it, mobile users might download desktop-sized images.
Especially when using fill.
<div style={{ position: 'relative', height: '400px' }}>
<Image
src="/banner.jpg"
fill
style={{ objectFit: 'cover' }}
// "If viewport < 768px, image is 100vw wide. Else, it's 50vw."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
This tells Next.js which srcset to generate.
If omitted, defaults to 100vw, meaning 1920px image on a 320px phone. Bad.
Deep Dive: CLS & blurDataURL
1. Preventing CLS (Layout Shift)
<img> causes content to jump when loading finishes.
next/image reserves space using dimensions or placeholders. Zero layout shift.
2. Remote Image Blur
Next.js cannot auto-generate blur placeholders for remote URLs during build.
You must provide blurDataURL manually (Base64 string).
<Image
src={remoteUrl}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Generate with 'plaiceholder'
/>
Application: AVIF & Loaders
AVIF Format
Enable AVIF (better than WebP) in config:
// next.config.js
images: {
formats: ['image/avif', 'image/webp'],
}
Custom Loader
If you use Cloudinary or Imgix, use a loader to construct their specific URLs.
const myLoader = ({ src, width, quality }) => {
return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}
7. Deep Dive: Sharp vs Squoosh (Engine Swap)
Next.js uses Squoosh (WASM) by default. It's portable but slow. For production, you MUST serve using Sharp (Native).
npm install sharp
Just installing this boosts image optimization speed by 3-4x. Since Serverless Functions (Lambda) are billed by execution time, faster compression = Lower Bills.
8. Deep Dive: Device Sizes & Image Sizes
Control exactly what variants are generated in next.config.js.
module.exports = {
images: {
// Viewport width breakpoints (responsive/fill)
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Image width breakpoints (fixed/intrinsic)
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
If your CSS breakpoints are just 600px and 1000px, you don't need 8 different variants.
Tailor deviceSizes to your CSS framework to save storage and build time.
9. Case Study: Dieting a 10MB Landing Page
I optimized a startup's landing page. Marketing uploaded 5 uncompressed raw photos (3MB each) into a Carousel. Total 15MB.
The Problem
- Using plain
<img>. - Eager loading hidden slides (2nd, 3rd images).
- LCP 6.8s. Bounce rate 70%.
The Fix
- Format:
next/imageauto-converted to WebP. 15MB -> 1.2MB (-92%). - Lazy: Only the 1st slide got
priority. Others stayedlazy. - Sizes: Added
sizes="100vw". Mobile users downloaded 400px versions, not 4000px.
The Result
LCP dropped from 6.8s to 0.9s. Bounce rate dropped to 20%. CRO (Conversion Rate Optimization) skyrocketed. Performance isn't just vanity; it's Revenue.
10. FAQ
Q: What if I don't know the width/height (CMS)?
A: Use fill. But ensure the parent div has position: relative and a defined height (or aspect-ratio CSS).
Q: Images are slow in npm run dev.
A: Next.js optimizes images on-demand and doesn't aggressively cache in dev mode. In npm start (production), aggressive caching kicks in.
Q: Should I use Image for SVGs?
A: No. SVGs are vectors. Just use <img> or inline <svg>. next/image might rasterize them, which ruins the infinite scaling.
11. Deep Dive: Art Direction (The picture element)
Sometimes, resizing isn't enough.
- Desktop: A wide panoramic shot of a landscape.
- Mobile: If you just shrink it, the landscape becomes a tiny strip. You want to Crop it to show just the tree in the center.
next/image handles resizing, but not Cropping (Art Direction).
For this, you still need standard HTML/CSS techniques, or use two Image components with CSS display toggles.
<div className="mobile-only">
<Image src="/tree-cropped.jpg" ... />
</div>
<div className="desktop-only">
<Image src="/landscape.jpg" ... />
</div>
This ensures the user sees the Right Content, not just the Right Size.
12. Conclusion
Images are the heaviest assets on the web. Optimizing them is the single highest-ROI activity for web performance.
next/image is a "Magic Box."
- It negotiates formats (WebP/AVIF).
- It resizes on the fly.
- It lazy loads by default.
- It prevents layout shifts.
All you have to do is replace <img> with <Image> and add sizes.
It's a small change for a developer, but a giant leap for User Experience.
Summary:
<img> is a 3MB bomb. Use next/image to serve optimized formats and sizes. Don't forget the sizes prop!