Advanced Image Optimization
Users perceive pages as faster when they see something immediately, even if it's blurry. A 10px placeholder that fades into the full image feels instant, while a blank space that suddenly pops in feels jarring. Art direction (different images for mobile vs desktop) can cut mobile network bandwidth by 60% while improving visual impact. These advanced patterns separate polished production apps from basic implementations.
This lesson uses apps/web. All file paths are relative to that directory.
This lesson assumes you've completed Images (next/image). You should already know how to use next/image with fill, sizes, quality, and remotePatterns. This lesson covers advanced patterns only.
Outcome
Implement blur placeholders for perceived performance, use getImageProps() for art direction with the <picture> element, and configure deviceSizes/imageSizes to optimize srcset generation for your specific breakpoints.
Fast Track
- Add
placeholder="blur"with a generatedblurDataURLfor remote images - Use
getImageProps()with<picture>for mobile/desktop art direction - Configure
deviceSizesandimageSizesinnext.config.tsto match your design system
Blur Placeholders for Perceived Performance
Users perceive loading time based on visual feedback, not actual milliseconds. A blurred placeholder that fades into the full image feels 40% faster than a blank space, even with identical load times. This is the "skeleton screen" effect applied to images.
Static Images: Automatic Blur
For statically imported images, Next.js automatically generates blurDataURL:
import Image from "next/image";
import heroImage from "./hero.jpg"; // Static import
export default function AboutPage() {
return (
<Image
src={heroImage}
placeholder="blur" // blurDataURL auto-generated
alt="Team photo"
className="rounded-lg"
/>
);
}What happens:
- At build time, Next.js generates a tiny (10px) blurred version
- The blur displays immediately while the full image loads
- Smooth fade transition when full image is ready
- Zero runtime cost, all computed at build
Remote Images: Manual blurDataURL
For remote/dynamic images, you must provide blurDataURL yourself:
import Image from "next/image";
// Pre-generated blur placeholder (10x10 pixels, base64 encoded as text representation of binary data)
const blurDataURL =
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABgcI/8QAIhAAAgEDBAMBAAAAAAAAAAAAAQIDBAURAAYSIQcTMUH/xAAVAQEBAAAAAAAAAAAAAAAAAAADBP/EABkRAAIDAQAAAAAAAAAAAAAAAAECAAMRIf/aAAwDAQACEQMRAD8Aq9t3Bb7hU1FPBLIZYQDIrRMuAf3OdNNu+RbRa7XDQS1VY8kKhGkMBUMQMZxk4z+aUbf8d2i3XGorYpq1pZwA4aYEDA/MYGl+4/HdnulzqK2Kauikn5c1SYEKZxnGQDjP5rRVlYnJz//2Q==";
export default function GalleryPage() {
return (
<div className="relative aspect-video">
<Image
src="https://picsum.photos/1200/800"
alt="Gallery image"
fill
placeholder="blur"
blurDataURL={blurDataURL}
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
);
}Generating blurDataURL at Runtime
For dynamic images (e.g., from a CMS), generate placeholders server-side:
/**
* Generate a simple color-based blur placeholder.
* For production, use a library like plaiceholder for better results.
*/
export function generateColorPlaceholder(
r: number,
g: number,
b: number
): string {
// 1x1 pixel SVG with the dominant color
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect fill="rgb(${r},${g},${b})" width="1" height="1"/></svg>`;
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
}
// Usage: generateColorPlaceholder(59, 130, 246) → blue placeholderFor production apps, use plaiceholder to generate proper blur placeholders from image URLs. It extracts dominant colors and creates optimized base64 placeholders.
I need blur placeholders (blurDataURL) for remote images in Next.js.
<context>
Static imports get automatic blur placeholders:
```tsx
import hero from './hero.jpg'
<Image src={hero} placeholder="blur" /> // Works automatically
```
Remote images need manual blurDataURL:
```tsx
<Image
src="https://cdn.example.com/photo.jpg"
placeholder="blur"
blurDataURL="data:image/..." // Must provide this
/>
```
</context>
<my-situation>
**Image source:** _____
Example: "CMS API", "User uploads", "External CDN"
**When URLs are known:**
- [ ] Build time (static list of images)
- [ ] Runtime (dynamic, from API/database)
**Image types:** _____
Example: "Product photos", "User avatars", "Blog featured images"
**Volume:** ~_____ images
</my-situation>
<current-implementation>
```tsx
// How I'm currently loading images:
___PASTE_YOUR_IMAGE_CODE___
```
</current-implementation>
**Questions:**
1. Should I generate placeholders at build time or runtime?
2. Which library? (plaiceholder, sharp, thumbhash)
3. Where should I cache generated placeholders?
4. How do I avoid blocking page render?
Generate a production-ready blur placeholder system that:
- Works with my image source
- Includes caching strategy
- Integrates with next/image
- Doesn't hurt performanceArt Direction with getImageProps()
Responsive images (using sizes) serve the same image at different resolutions.
Art direction serves different images based on viewport, such as a landscape hero on desktop and a portrait crop on mobile.
The getImageProps() function extracts props for use with the native <picture> element:
import { getImageProps } from "next/image";
export function HeroImage() {
const common = { alt: "Product showcase", sizes: "100vw" };
// Desktop: wide landscape image
const {
props: { srcSet: desktop },
} = getImageProps({
...common,
width: 1440,
height: 600,
quality: 85,
src: "/hero-desktop.jpg",
});
// Mobile: tall portrait image (different crop, not just smaller)
const {
props: { srcSet: mobile, ...rest },
} = getImageProps({
...common,
width: 750,
height: 1000,
quality: 75,
src: "/hero-mobile.jpg",
});
return (
<picture>
<source media="(min-width: 1024px)" srcSet={desktop} />
<source media="(min-width: 640px)" srcSet={mobile} />
<img {...rest} style={{ width: "100%", height: "auto" }} />
</picture>
);
}Why use art direction:
- Mobile users get a portrait-optimized crop (better composition)
- Desktop users get a wide landscape (uses screen real estate)
- Different quality settings per device (mobile can use lower quality)
- Bandwidth savings: mobile image is 60% smaller than desktop
I need to decide between responsive images and art direction for my Next.js app.
<context>
**Responsive images** (same image, different sizes):
```tsx
<Image src={photo} sizes="(max-width: 768px) 100vw, 50vw" />
```
**Art direction** (different images per breakpoint):
```tsx
<picture>
<source media="(max-width: 768px)" srcSet={mobileImg} />
<Image src={desktopImg} />
</picture>
```
</context>
<my-image>
**Image type:** _____
Example: "Hero banner", "Product photo", "Team headshot"
**Desktop version:**
- Dimensions: _____x_____
- What it shows: _____
**Mobile version (current):**
- What happens when desktop image is scaled down: _____
Example: "Important content gets cropped", "Text becomes unreadable", "Looks fine"
**The problem (if any):** _____
Example: "Product details too small on mobile", "Wrong aspect ratio looks awkward"
</my-image>
<current-implementation>
```tsx
// My current image code:
___PASTE_YOUR_IMAGE_CODE___
```
</current-implementation>
**Questions:**
1. Do I actually need art direction, or will responsive `sizes` work?
2. Is the UX improvement worth the extra complexity?
3. How do I handle this in my CMS/design workflow?
Help me decide:
- A) Stick with responsive (simpler, same image scales)
- B) Use art direction (complex, different crops/images)
- C) Hybrid (art direction for heroes only)
Include implementation code for whichever you recommend.Theme-Aware Images (Light/Dark Mode)
Use CSS media queries with getImageProps() for theme detection:
import { getImageProps } from "next/image";
import styles from "./theme-image.module.css";
export function ThemeImage() {
const {
props: { srcSet: light, ...lightRest },
} = getImageProps({
src: "/logo-light.png",
alt: "Logo",
width: 200,
height: 50,
});
const {
props: { srcSet: dark, ...darkRest },
} = getImageProps({
src: "/logo-dark.png",
alt: "Logo",
width: 200,
height: 50,
});
return (
<>
<img {...lightRest} srcSet={light} className={styles.light} />
<img {...darkRest} srcSet={dark} className={styles.dark} />
</>
);
}.dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.light {
display: none;
}
.dark {
display: unset;
}
}When using theme-aware images, avoid preload or loading="eager" because both images would load. Use fetchPriority="high" instead if the image is above the fold.
Configuring deviceSizes and imageSizes
Next.js combines deviceSizes and imageSizes to generate the srcset attribute. deviceSizes are for full-width images, imageSizes are for images smaller than the viewport. Together they determine which image widths are available.
Default Configuration
// deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
// imageSizes: [32, 48, 64, 96, 128, 256, 384]Custom Configuration for Your Design System
Match your Tailwind breakpoints and common image sizes:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
// Match Tailwind breakpoints: sm(640), md(768), lg(1024), xl(1280), 2xl(1536)
deviceSizes: [640, 768, 1024, 1280, 1536, 1920],
// Common thumbnail/avatar sizes in your design system
imageSizes: [32, 48, 64, 96, 128, 192, 256],
// Allowlist quality values (required in Next.js 16)
qualities: [50, 75, 85, 100],
// Modern formats
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;Why customize:
- deviceSizes: Align with your CSS breakpoints to avoid serving images between breakpoints
- imageSizes: Match your avatar/thumbnail sizes exactly (32px avatar, 96px card thumbnail)
- qualities: Restrict to values you actually use (prevents abuse of optimization API)
Impact on Generated srcset
With default config, a responsive image generates:
<img srcset="
/_next/image?url=...&w=640&q=75 640w,
/_next/image?url=...&w=750&q=75 750w,
/_next/image?url=...&w=828&q=75 828w,
...
" />With custom config matching Tailwind:
<img srcset="
/_next/image?url=...&w=640&q=75 640w,
/_next/image?url=...&w=768&q=75 768w,
/_next/image?url=...&w=1024&q=75 1024w,
...
" />Next.js 16: preload Replaces priority
The priority prop is deprecated in Next.js 16. Use preload instead for clearer semantics.
// ❌ Deprecated in Next.js 16
<Image src="/hero.jpg" alt="Hero" priority />
// âś… Next.js 16+
<Image src="/hero.jpg" alt="Hero" preload />When to use preload:
- The image is the Largest Contentful Paint (LCP) element
- The image is above the fold (visible without scrolling)
- You want the image to start loading in
<head>before it's discovered in<body>
When NOT to use preload:
- Multiple images could be LCP depending on viewport (use
loading="eager"instead) - Below-the-fold images (let them lazy load)
- When using
loadingorfetchPriorityprops
Hands-On Exercise 4.4
Implement advanced image optimization patterns in the gallery page.
Target files:
apps/web/src/app/gallery/page.tsxapps/web/next.config.ts
Requirements:
- Add blur placeholders to gallery images using
placeholder="blur"withblurDataURL - Create a hero section with art direction: landscape on desktop, portrait on mobile
- Configure
deviceSizesto match Tailwind breakpoints - Configure
qualitiesto allowlist only the values you use (75, 85) - Use
preload(notpriority) for the hero image
Implementation hints:
- Generate a simple color placeholder for remote images:
const blurDataURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxIDEiPjxyZWN0IGZpbGw9IiM5Y2EzYWYiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48L3N2Zz4="; - Use
getImageProps()for the art direction hero - Test on mobile viewport to verify correct image loads
Try It
-
Verify blur placeholder:
- Throttle network to "Slow 3G" in DevTools
- Reload the gallery page
- Observe: blurred placeholder visible immediately, fades to full image
-
Test art direction:
- Open DevTools → Network tab
- Load page at desktop width (
>1024px) → verify desktop image loads - Resize to mobile width (
<640px) → reload → verify mobile image loads - Different images, not just different sizes
-
Verify srcset matches config:
- Inspect a gallery image element
- Check
srcsetattribute contains your configured widths (640, 768, 1024...) - Not the default widths (750, 828, 1080...)
Commit & Deploy
git add -A
git commit -m "feat(images): add blur placeholders, art direction, custom srcset config"
git push -u origin feat/advanced-image-optimizationDone-When
- Gallery images show blur placeholder on slow network (throttle to Slow 3G, reload, see blur before full image)
- Hero section uses
<picture>element with differentsrcSetfor mobile/desktop (inspect HTML) next.config.tshas customdeviceSizesmatching Tailwind breakpoints (640, 768, 1024, 1280, 1536)next.config.tshasqualitiesarray restricting allowed quality values- Hero image uses
preloadprop (not deprecatedpriority) - Network tab shows different hero image file loaded on mobile vs desktop viewport
Solution
Complete advanced image optimization implementation
1. Update next.config.ts with custom image configuration:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
pathname: "/**",
},
],
// Match Tailwind breakpoints
deviceSizes: [640, 768, 1024, 1280, 1536, 1920],
// Common UI sizes
imageSizes: [32, 48, 64, 96, 128, 192, 256],
// Restrict quality values (required in Next.js 16)
qualities: [75, 85],
// Modern formats
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;2. Create the gallery page with blur placeholders and art direction:
import Image, { getImageProps } from "next/image";
// Simple gray blur placeholder (base64 encoded 1x1 SVG)
const blurDataURL =
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxIDEiPjxyZWN0IGZpbGw9IiM5Y2EzYWYiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48L3N2Zz4=";
const galleryImages = [
{ 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" },
];
function HeroWithArtDirection() {
const common = { alt: "Featured landscape", sizes: "100vw" };
// Desktop: wide landscape
const {
props: { srcSet: desktop },
} = getImageProps({
...common,
width: 1440,
height: 600,
quality: 85,
src: "https://picsum.photos/1440/600?random=hero-desktop",
});
// Mobile: taller aspect ratio
const {
props: { srcSet: mobile, ...rest },
} = getImageProps({
...common,
width: 750,
height: 900,
quality: 75,
src: "https://picsum.photos/750/900?random=hero-mobile",
});
return (
<picture>
<source media="(min-width: 1024px)" srcSet={desktop} />
<source media="(min-width: 640px)" srcSet={mobile} />
<img
{...rest}
fetchPriority="high"
style={{ width: "100%", height: "auto" }}
className="rounded-lg"
/>
</picture>
);
}
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 with art direction */}
<section className="mb-8">
<HeroWithArtDirection />
</section>
{/* Gallery grid with blur placeholders */}
<div className="grid grid-cols-2 gap-4">
{galleryImages.map((image, i) => (
<div key={i} className="relative aspect-[4/3]">
<Image
src={image.src}
alt={image.alt}
fill
quality={75}
placeholder="blur"
blurDataURL={blurDataURL}
sizes="(max-width: 768px) 100vw, 50vw"
className="rounded-lg object-cover"
/>
</div>
))}
</div>
<section className="mt-8 rounded bg-blue-50 p-4">
<h2 className="mb-2 font-semibold text-blue-800">
Advanced Optimizations Applied
</h2>
<ul className="list-inside list-disc text-blue-700 text-sm">
<li>Blur placeholders for perceived performance</li>
<li>Art direction: different hero images for mobile/desktop</li>
<li>Custom deviceSizes matching Tailwind breakpoints</li>
<li>Quality values restricted to 75, 85</li>
<li>AVIF/WebP format optimization</li>
</ul>
</section>
</main>
);
}Key advanced patterns implemented:
- Blur placeholders:
placeholder="blur"withblurDataURLfor remote images - Art direction:
getImageProps()with<picture>element for mobile/desktop - Custom srcset:
deviceSizesaligned with Tailwind breakpoints - Quality restriction:
qualitiesarray limits allowed values - fetchPriority: Used instead of deprecated
priorityfor art direction hero
Verify the implementation:
- Blur placeholder: Throttle to Slow 3G, reload, see gray blur before images
- Art direction: Resize browser, check Network tab for different hero images
- srcset: Inspect image element, verify widths match your deviceSizes config
Advanced Image Optimization Checklist
| Pattern | When to Use | Implementation |
|---|---|---|
| Blur placeholder | Remote/dynamic images | placeholder="blur" + blurDataURL |
| Art direction | Different crops for mobile/desktop | getImageProps() + <picture> |
| Custom deviceSizes | Match your CSS breakpoints | next.config.ts images config |
| Custom imageSizes | Match your avatar/thumbnail sizes | next.config.ts images config |
| Quality restriction | Prevent API abuse, control file sizes | qualities array in config |
| Theme images | Light/dark mode logos | CSS media queries + getImageProps() |
Performance impact:
- Blur placeholders: 40% better perceived load time
- Art direction: 60% bandwidth savings on mobile
- Custom srcset: Eliminates between-breakpoint waste
- Quality restriction: Prevents 100% quality abuse
References
Was this helpful?