next/image: automatic image optimization
A plain <img> tag ships whatever file is referenced, at whatever size it happens to be, with no help from the framework. next/image's Image component resizes, compresses, and lazy-loads automatically — this part adds a cover image to a post using it.
A local image
public/
images/
hello-app-router.jpg// app/posts/[slug]/page.tsx
import Image from "next/image";
<Image
src="/images/hello-app-router.jpg"
alt={post.title}
width={800}
height={400}
priority
/>width and height aren't optional the way they'd be on a plain <img> — Image uses them to reserve the correct space in the layout before the image itself has loaded, which is what prevents a Cumulative Layout Shift as it comes in. alt is required by the component's own TypeScript types, not just a best practice here — a missing alt is a type error, not just an accessibility oversight. priority tells Next.js to load this specific image eagerly rather than lazily, appropriate for an image that's likely to be a post's Largest Contentful Paint element.
What Image actually does over a plain img tag
- Resizing — serves a size appropriate to the viewport requesting it, not the full original file, unless it's already smaller
- Modern formats — automatically serves WebP or AVIF to browsers that support them, falling back to the original format otherwise
- Lazy loading by default — every
Imagenot markedpriorityonly loads once it's about to scroll into view - Layout stability — the
width/heightreservation from above, preventing layout shift as images load in
Externally hosted images
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ hostname: "images.example.com" },
],
},
};
export default nextConfig;next/image refuses to optimize an image from a host it doesn't explicitly trust — without this, an Image pointed at https://images.example.com/cover.jpg throws a build-time error rather than silently serving it unoptimized. This is a deliberate safeguard: Next.js's image optimizer fetches and processes whatever URL is given it, and an unrestricted allowlist would let the Image component be pointed at arbitrary third-party URLs.
Filling a container without fixed dimensions
<div style={{ position: "relative", width: "100%", height: 300 }}>
<Image src="/images/hello-app-router.jpg" alt={post.title} fill style={{ objectFit: "cover" }} />
</div>fill is the alternative to fixed width/height — the image stretches to fill its nearest positioned ancestor instead, which needs position: relative (or similar) set explicitly for fill to have a box to actually fill.
Using a plain <img> tag for an image that's genuinely part of the page's content, out of habit or to avoid the remotePatterns configuration step. It works, but skips every optimization above — for a blog with real cover images, that's a meaningful, easily avoidable hit to both load time and Core Web Vitals.
Next: next/link has linked every route in this series so far without ever covering what it actually does beyond rendering an anchor tag — prefetching, specifically.