Image Optimization with next/image
Step 29 of 31 — Next.js Tutorial Series | Source code for this step
What You Will Build
Our blog posts show the author name, but there's no visual avatar. In this step, you will:
- Add author avatars using placeholder images from an external service
- Use the
<Image>component fromnext/imagefor automatic optimization - Configure
next.config.tsto allow external image domains - Understand how Next.js optimizes images — resizing, format conversion, lazy loading
Goal: Learn the next/image component — the standard way to handle images in Next.js applications.
Table of Contents
- Why next/image Instead of img
- The Image Component API
- Configuring External Image Domains
- Adding Avatars to the Post Detail Page
- Adding Avatars to the Posts List
- How Image Optimization Works
- Local vs External Images
- Verify the Images
- Summary & Key Takeaways
Why next/image Instead of img
The standard HTML <img> tag works but has problems:
| Problem | <img> | <Image> from next/image |
|---|---|---|
| Size | Serves the original file (could be 5MB) | Automatically resizes to the displayed size |
| Format | Serves the original format (PNG, JPEG) | Converts to WebP/AVIF (smaller, modern formats) |
| Loading | Loads all images immediately | Lazy-loads images (only loads when near the viewport) |
| Layout shift | Image "jumps" when it loads | Reserves space — no layout shift (you specify width/height) |
| Responsive | Manual work with srcset | Automatic srcset generation |
The <Image> component solves all of these automatically. It's one of the biggest performance wins Next.js offers with minimal effort.
The Image Component API
import Image from "next/image";
<Image
src="/avatar.jpg" // Image source — local path or URL
alt="User avatar" // Alt text — required for accessibility
width={40} // Display width in pixels
height={40} // Display height in pixels
className="rounded-full" // Standard CSS classes work
/>
Required props
| Prop | Purpose |
|---|---|
src | Image source — a local path (e.g., /avatar.jpg) or external URL |
alt | Accessibility text — describes the image for screen readers |
width | Display width in pixels (required for external images) |
height | Display height in pixels (required for external images) |
Common optional props
| Prop | Purpose | Default |
|---|---|---|
priority | Load immediately (skip lazy loading) | false |
placeholder | Show a placeholder while loading | "empty" |
quality | Image quality (1-100) | 75 |
sizes | Responsive sizes hint | auto |
priority should be set to true for images that are above the fold — visible without scrolling. For example, the hero image or the first avatar the user sees. This tells the browser to download it immediately instead of waiting.
Configuring External Image Domains
By default, next/image only optimizes local images (files in your public/ directory). To use external images, you must configure the allowed domains in next.config.ts.
We will use UI Avatars (ui-avatars.com) — a free service that generates letter-based avatars from a name. For example:
https://ui-avatars.com/api/?name=Alice&background=0d9488&color=fff&size=80
This generates a teal circle with the letter "A" — no image uploads needed.
Update next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "ui-avatars.com",
pathname: "/api/**",
},
],
},
};
export default nextConfig;
Why remotePatterns instead of domains?
The domains config is deprecated. remotePatterns is more secure because you can specify:
protocol— onlyhttps(nothttp)hostname— the exact domainpathname— which paths are allowed (e.g., only/api/**)
This prevents someone from injecting a malicious image URL that happens to be on the same domain.
Adding Avatars to the Post Detail Page
Update app/posts/[id]/page.tsx to show an avatar next to the author name.
Find the author info section and add an avatar:
import Image from "next/image";
Then in the JSX, replace the author line:
<div className="flex items-center gap-3 mb-6">
<Image
src={`https://ui-avatars.com/api/?name=${encodeURIComponent(post.author?.name ?? "U")}&background=0d9488&color=fff&size=80`}
alt={post.author?.name ?? "Unknown"}
width={40}
height={40}
className="rounded-full"
/>
<div>
<p className="text-sm font-medium text-gray-800">
{post.author?.name ?? "Unknown"}
</p>
<p className="text-xs text-gray-400">
{new Date(post.createdAt).toLocaleDateString()}
</p>
</div>
</div>
What's happening
encodeURIComponent— ensures the author name is URL-safe (handles spaces and special characters)background=0d9488— teal background color (matches our app's theme)color=fff— white textsize=80— the source image is 80x80px (2x the display size for retina screens)width={40}andheight={40}— the display size is 40x40pxclassName="rounded-full"— makes it circular
Why is size=80 but width={40}?
For sharp images on high-DPI (retina) screens, you want the source image to be at least 2x the display size. The <Image> component handles the resizing — it serves a 40px image to regular screens and an 80px image to retina screens.
Adding Avatars to the Posts List
Update app/posts/PostList.tsx to show avatars in the posts listing. Add the import:
import Image from "next/image";
Then update the post card's author section:
<div className="flex items-center gap-2 mb-3">
<Image
src={`https://ui-avatars.com/api/?name=${encodeURIComponent(post.author?.name ?? "U")}&background=0d9488&color=fff&size=64`}
alt={post.author?.name ?? "Unknown"}
width={32}
height={32}
className="rounded-full"
/>
<p className="text-sm text-gray-500">
{post.author?.name ?? "Unknown"} ·{" "}
{new Date(post.createdAt).toLocaleDateString()}
</p>
</div>
Here we use a smaller avatar (32x32 display, 64px source) since the listing cards are more compact than the detail page.
How Image Optimization Works
When a browser requests an image through next/image, Next.js does the following:
Browser requests /posts/1
│
▼
<Image src="https://ui-avatars.com/..." width={40} height={40} />
│
▼
Next.js generates: <img srcset="/_next/image?url=...&w=40 1x, /_next/image?url=...&w=80 2x" />
│
▼
Browser requests /_next/image?url=https://ui-avatars.com/...&w=80&q=75
│
▼
Next.js Image Optimization API:
1. Fetches the image from ui-avatars.com
2. Resizes to 80px width
3. Converts to WebP format
4. Caches the result
5. Sends the optimized image to the browser
On subsequent requests
The optimized image is cached. The next browser that requests the same image gets the cached version — no re-fetch, no re-resize.
On Vercel
Vercel's Image Optimization runs at the edge — close to the user. Images are optimized once and cached globally. This is included in Vercel's free tier.
The generated HTML
If you inspect the rendered HTML, you'll see something like:
<img
alt="Alice"
loading="lazy"
width="40"
height="40"
decoding="async"
srcset="/_next/image?url=https%3A%2F%2Fui-avatars.com...&w=48&q=75 1x,
/_next/image?url=https%3A%2F%2Fui-avatars.com...&w=96&q=75 2x"
src="/_next/image?url=https%3A%2F%2Fui-avatars.com...&w=96&q=75"
class="rounded-full"
/>
Notice:
loading="lazy"— browser lazy-loads the imagedecoding="async"— browser decodes the image off the main threadsrcset— provides 1x and 2x versions for different screen densities- All URLs go through
/_next/image— Next.js's optimization proxy
Local vs External Images
| Local images | External images | |
|---|---|---|
| Source | Files in public/ or imported | URLs from external services |
| Config needed? | No | Yes — must add to remotePatterns |
width/height required? | No — auto-detected from the file | Yes — must be specified |
placeholder="blur" | Supported — auto-generates blur hash | Not supported (no access to file at build time) |
Local image example
import profilePic from "@/images/profile.jpg";
<Image src={profilePic} alt="Profile" />
// width and height are automatically detected from the file
External image example (what we're doing)
<Image
src="https://ui-avatars.com/api/?name=Alice"
alt="Alice"
width={40}
height={40}
/>
// width and height must be specified
For our blog app, we use external images because avatars are generated dynamically based on the user's name. If you had static images (e.g., a logo), you'd import them locally.
Verify the Images
- Start the dev server:
pnpm dev - Visit
/posts— each post card should show a circular avatar with the author's initial - Click on a post — the detail page should show a larger avatar with the author name and date next to it
- Open DevTools → Network tab → filter by "image" — you should see requests to
/_next/image(not directly toui-avatars.com) - Check the image format — it should be
webp(not the original PNG from ui-avatars)
Troubleshooting
If images don't appear:
- Check
next.config.ts— make sureui-avatars.comis inremotePatterns - Restart the dev server after changing
next.config.ts— config changes require a restart - Check the browser console for errors — you might see "Invalid src prop" if the domain isn't configured
Summary & Key Takeaways
| Concept | Details |
|---|---|
<Image> vs <img> | <Image> auto-optimizes: resizes, converts to WebP, lazy-loads, prevents layout shift |
next/image import | import Image from "next/image" — the component |
width and height | Required for external images — reserves space to prevent layout shift |
priority | Set to true for above-the-fold images — skips lazy loading |
remotePatterns | Configure allowed external image domains in next.config.ts |
/_next/image | All images go through Next.js's optimization proxy |
| Retina support | Serve 2x source images — Next.js generates srcset automatically |
| Caching | Optimized images are cached on the server — subsequent requests are instant |
| Local images | Import directly — width/height auto-detected, blur placeholder supported |
What is Next
In Step 30, we will explore Prisma Query Insights — a dashboard built into Prisma Postgres that helps you identify slow queries, N+1 problems, and missing indexes.