Images (next/image)
Your hero image is 2.4MB and takes 6 seconds to load on mobile. Your gallery causes layout shift every time an image pops in, shoving content around and frustrating users. Lighthouse screams about LCP and CLS.
You could manually resize images, convert formats, add loading attributes, calculate sizes... or let next/image handle all of it automatically.
Outcome
A hero image with priority for LCP optimization, a responsive gallery with correct sizes prop, and zero CLS from images. Automatic format optimization and lazy loading for below-fold images.
Fast Track
- Replace
<img>with<Image>fromnext/image. - Add
priorityto hero/LCP images to preload immediately. - Set
sizesprop for responsive images and reserve space withwidth/heightorfill.
Hands-On Exercise 3.4
The starter repo includes a gallery page at apps/web/src/app/gallery/page.tsx with raw <img> tags. Your task is to optimize it.
Requirements:
- Convert the gallery images in
apps/web/src/app/gallery/page.tsxfrom<img>to<Image>. - Add a hero image with
priorityprop for LCP optimization. - Set
sizesprop correctly for the 2-column grid layout. - Configure
remotePatternsinnext.config.tsfor the external image source (picsum.photos). - Use
fillwith aspect ratio container to reserve space and prevent CLS.
Implementation hints:
priorityprop preloads above-fold images (LCP candidates).- Lazy loading automatic for below-fold images.
sizesprop tells browser which image size to fetch:"(max-width: 768px) 100vw, 50vw".- Quality 75-85 is sweet spot - 100 wastes bandwidth with minimal visual gain.
- Space reservation with
width/heightorfillwith container prevents CLS. - Use
remotePatternsinnext.config.tsfor external images. - Automatic format optimization: WebP, AVIF based on browser support.
Try It
-
Measure LCP improvement:
- Open DevTools Performance tab, record page load.
- Verify hero image is LCP element and loads quickly with
priority. - Target: LCP < 2.5s.
-
Verify CLS = 0:
- Throttle network to "Slow 3G" in DevTools.
- Scroll through page - no layout shift as images load.
- Space reserved with
width/heightorfillwith container.
-
Check format optimization:
- Open Network tab, inspect image requests.
- Verify WebP or AVIF format served (not JPEG/PNG).
- Responsive sizes match viewport with correct
sizesprop.
Commit & Deploy
git add -A
git commit -m "feat(advanced): optimize images with next/image and sizes"
git push -u origin feat/advanced-image-optimizationDone-When
- View page source: find
<link rel="preload" ... fetchpriority="high">for hero image (confirmspriorityprop works) - Open DevTools Network tab: hero image loads early in waterfall, before other images
- Gallery images show correct responsive loading: resize browser and verify smaller images fetched on mobile viewports
- DevTools Network tab: images served as
image/webporimage/avif(not jpg/png) from/_next/image - Lighthouse Performance audit: CLS score shows 0 or < 0.1 for images (no layout shift during load)
- Throttle to "Slow 3G" and scroll: below-fold gallery images lazy load as they enter viewport (not all at once)
Solution
Click to reveal solution
First, configure remote patterns for the external image source:
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'picsum.photos',
},
],
formats: ['image/avif', 'image/webp'],
},
}
export default configThen convert the gallery page to use next/image:
import Image from 'next/image'
const images = [
{ src: 'https://picsum.photos/800/600?random=1', alt: 'Mountain landscape' },
{ src: 'https://picsum.photos/800/600?random=2', alt: 'Ocean sunset' },
{ src: 'https://picsum.photos/800/600?random=3', alt: 'Forest path' },
{ src: 'https://picsum.photos/800/600?random=4', alt: 'City skyline' },
]
export default function GalleryPage() {
return (
<main className="mx-auto max-w-4xl p-8">
<h1 className="mb-8 font-bold text-3xl">Photo Gallery</h1>
{/* Hero image with priority for LCP */}
<div className="relative mb-8 aspect-video w-full">
<Image
src="https://picsum.photos/1200/600?random=hero"
alt="Featured landscape"
fill
priority // Preload for LCP optimization
quality={85}
sizes="(max-width: 896px) 100vw, 896px"
className="rounded-lg object-cover"
/>
</div>
{/* Gallery grid with responsive images */}
<div className="grid grid-cols-2 gap-4">
{images.map((image, i) => (
<div key={i} className="relative aspect-[4/3]">
<Image
src={image.src}
alt={image.alt}
fill
quality={80}
sizes="(max-width: 768px) 50vw, 400px"
className="rounded-lg object-cover"
// Lazy loading automatic for below-fold images
/>
</div>
))}
</div>
<section className="mt-8 rounded bg-green-100 p-4">
<h2 className="mb-2 font-semibold text-green-800">Performance Optimizations Applied</h2>
<ul className="list-inside list-disc text-green-700 text-sm">
<li>Images served as WebP/AVIF from /_next/image</li>
<li>Hero preloaded with priority prop</li>
<li>Responsive sizes prevent over-fetching</li>
<li>Space reserved with fill + aspect ratio (no CLS)</li>
<li>Below-fold images lazy load automatically</li>
</ul>
</section>
</main>
)
}References
Was this helpful?