What you'll learn
By the end of this you'll know how to use next/image properly — correct dimensions, fill for responsive containers, remote images, and why priority matters — and how to load fonts with next/font without quietly wrecking your Lighthouse scores.
You'll also see the specific mistakes that hurt LCP and CLS, because those are the ones that actually bite people.
Who this is for
- You're building a Next.js site and images look wrong, slow, or jump around
- You've got decent code but Lighthouse mobile keeps coming back with sad numbers
- You're new to Next.js and want to get image/font setup right from the start
You can skip this if you already have a polished image pipeline with CDN optimization and font subsetting dialed in, and you're looking for edge-case stuff. For everyone else, keep reading — this will save you a few frustrating Lighthouse runs.
What is next/image?
It's a React component that replaces the plain <img> tag for content images. Behind the scenes it does a few things:
- Serves smaller file formats when the browser supports them
- Resizes images to roughly match the device
- Lazy-loads images below the fold automatically
- Reserves space in the layout so the page doesn't jump when the image arrives
Plain English version: it tries to serve the right-sized image at the right time without wrecking your layout.
A raw <img> tag does none of that automatically.
What is next/font?
It's the built-in way to load Google Fonts (and local fonts) in a way that avoids the flash of unstyled text, reduces extra network round-trips, and keeps your CLS in a reasonable range.
Plain English: no ugly font-swap mid-load, and you don't have to configure it by hand.
Prerequisites
- An existing Next.js App Router project (the App Router basics guide is a good starting point if you need one)
- Node.js 20+
- A couple of image files to test with — grab any JPEG or PNG
If you don't have a project yet:
npx create-next-app@latest image-lab
cd image-lab
npm run dev
Setup from zero
Step 1 — Drop an image into public/
The public/ folder (create-next-app creates it automatically) maps to the site root. Put an image in there:
public/hero.jpg
That file becomes available at /hero.jpg in your app.
Step 2 — Use next/image on the home page
In src/app/page.tsx:
import Image from "next/image";
export default function HomePage() {
return (
<main>
<h1>Image lab</h1>
<Image
src="/hero.jpg"
alt="A wide landscape photo used for testing"
width={1200}
height={630}
priority
/>
</main>
);
}
Step 3 — Confirm it works
Refresh http://localhost:3000. The image should appear. Inspect the HTML and you'll see Next.js generated an optimized <img> with a URL through its image pipeline.
If you get an error: check the terminal output. Nine times out of ten it's a filename typo, the file is in the wrong folder, or you imported from the wrong package.
Core idea / mental model
Browsers measure how fast the main visible content appears. That's LCP — Largest Contentful Paint. For most blogs and landing pages, the LCP element is either a large heading or the hero image. It's usually the hero.
Bad pattern (what most first-time Next.js sites look like):
- Huge unoptimized JPEG downloaded on every device
- No width/height, so the page jumps when the image loads
- A custom font that appears late and shifts text around
Good pattern:
next/imagewith correct dimensions or a sized fill containerpriorityonly on the actual LCP imagenext/fontfor stable, brand-consistent typography
That's the whole mental model. next/image and next/font exist specifically to fight LCP and CLS. Keep that framing in your head and the API decisions make more sense.
Key terms
- LCP — Largest Contentful Paint: speed of the main visible content
- CLS — Cumulative Layout Shift: how much things move around as the page loads
priority— adds a preload hint for the LCP image (use sparingly)fill— image fills its positioned parent instead of having fixed dimensionssizes— tells the browser which image width to download at each screen size- Remote patterns — the allow-list for external image domains
Step-by-step: local images
Step 1 — Write meaningful alt text
<Image
src="/hero.jpg"
alt="Dashboard screenshot showing monthly revenue chart"
width={1200}
height={630}
/>
Don't leave alt empty on content images. Don't keyword-stuff it either. Describe what's actually in the image.
Step 2 — Give real dimensions when you know them
width and height let the browser reserve space before the image loads. That's how you prevent layout shift. And, honestly, it's also just good practice.
Step 3 — Use priority on exactly one image
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} priority />
priority adds a <link rel="preload"> hint so the browser fetches this image early in the request waterfall. But if you mark five images as priority on the same page, you've basically not prioritized anything.
Little tip: one priority per page, on the hero or whatever your LCP element is. That's it.
Working examples
Example A — responsive card image with fill
Sometimes you don't know exact dimensions — you just want the image to fill a container. Use fill:
import Image from "next/image";
export function PostCard() {
return (
<article>
<div style={{ position: "relative", width: "100%", height: 200 }}>
<Image
src="/hero.jpg"
alt="Article thumbnail"
fill
sizes="(max-width: 768px) 100vw, 33vw"
style={{ objectFit: "cover" }}
/>
</div>
<h2>How App Router routing works</h2>
</article>
);
}
Two things to notice here. The parent div has position: relative and a real height — without that, the image won't render. And sizes tells the browser "on small screens this image is full viewport width; on larger screens it's about a third."
That hint is how Next.js knows which resolution to serve. Skip it, and mobile downloads the same 1400px image as a desktop. Slow and wasteful.
Example B — remote image from a CMS
If your images live on another server (a CMS, Cloudinary, any CDN), you have to allow that host first. In next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.example.com",
pathname: "/**",
},
],
},
};
export default nextConfig;
Then use the full URL in your component:
<Image
src="https://images.example.com/posts/cover.jpg"
alt="Article cover"
width={800}
height={420}
/>
If you skip remotePatterns, Next.js errors out in development — which is annoying the first time but actually protects you from accidentally running an open image proxy.
Example C — fonts with next/font
In src/app/layout.tsx:
import type { ReactNode } from "react";
import { Inter, Source_Serif_4 } from "next/font/google";
import "./globals.css";
const sans = Inter({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
});
const serif = Source_Serif_4({
subsets: ["latin"],
variable: "--font-serif",
display: "swap",
});
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={sans.variable + " " + serif.variable}>
<body>{children}</body>
</html>
);
}
Then in globals.css:
body {
font-family: var(--font-sans), system-ui, sans-serif;
}
.article-body {
font-family: var(--font-serif), Georgia, serif;
}
CSS variables mean you can swap the actual font family in one place later. And display: "swap" keeps text visible with a fallback font while the real one loads, which protects CLS.
Little tip: two fonts is usually enough — a heading/UI font and a body font. Loading four or five Google Fonts because they look nice in the picker is a performance hole that's entirely avoidable.
Patterns and options
Quick reference for when to use what:
- Fixed
width/height→ you know the dimensions: hero images, OG thumbnails, avatars with known sizes fill→ image goes into a fluid container: article cards, grid layouts, anything responsivepriority→ only the main hero / LCP image, once per pageremotePatterns→ any image URL not served from yourpublic/foldernext/font→ every font on the site; don't load Google Fonts the old<link rel="stylesheet">way
When to NOT use next/image: decorative SVGs that are part of the UI, inline icons, anything where you need precise CSS transform control and the optimization pipeline just gets in the way.
Common mistakes
- Using a plain
<img>for content photos, then wondering why LCP is bad - Skipping
width/heightand getting a jumpy layout - Marking every hero-area image as
priority(defeats the purpose) - Missing
sizeson fill images — mobile downloads a full-desktop image for a 300px slot - Hotlinking remote images without
remotePatterns(errors in dev) - Uploading a 4000×3000 source JPEG for a 300px card thumbnail
- Loading five Google Fonts because the font picker made them all look tempting
- Adding a fancy fade-in animation on the LCP image, which visually delays it
Troubleshooting
- "hostname not configured" error — add the domain to
remotePatternsinnext.config.ts fillimage not visible — the parent element needsposition: relativeand an explicit height or width- Image looks stretched or squished — add
style={{ objectFit: "cover" }}(or"contain"if you want letterboxing) - Font not applying — make sure the CSS variable is on the
htmlelement and you're actually usingvar(--font-sans)in your CSS - LCP still slow — one
prioritymax, compress the source file before upload, check how much JS is loading on that page
Checklist before you ship
- [ ] Hero uses
next/imagewith real dimensions or a sized fill container - [ ] Only the actual LCP image has
priority - [ ] Responsive fill images have a
sizeshint - [ ] Remote image hosts are in
remotePatterns - [ ] All content images have meaningful alt text
- [ ] Fonts use
next/font, not a manual<link rel="stylesheet"> - [ ] No more than 1–2 primary font families loaded
- [ ] Lighthouse mobile run on at least one article or hub page
Practice task
In your lab project:
- Add a hero image with
priorityon the home page - Build a 3-card grid where each card uses
fill+ a sensiblesizeshint - Load Inter for UI text and one serif font for article body via
next/font - Apply serif to a
.article-bodyclass, sans to everything else - Run Lighthouse mobile — note the LCP and CLS values specifically
If you do all five, you've touched every part of the image/font system that matters in real projects.
FAQ
Does every image need next/image?
Nearly all content images, yes. Tiny decorative SVGs used as icons can stay as inline SVG or regular <img>. The key question is: does this image affect LCP or CLS? If yes, use next/image.
With fill, do I still need width and height?
Not on the Image component itself — but the parent must have an actual size. "Sized container" is the real requirement.
Is priority the same as loading="eager"?
Similar but not identical. priority adds a <link rel="preload"> hint so the browser starts fetching it early in the waterfall, before it even parses the image tag.
Can I use Cloudinary or ImageKit?
Yes. Add their hostname to remotePatterns and pass the full HTTPS URL. Both have custom loader integrations too if you need deeper control later.
What about background images in CSS?
They work, but for anything you care about for LCP or SEO, next/image is the better path. CSS backgrounds are harder for Lighthouse to measure and can't use the same optimization pipeline.
What to learn next
- Next.js App Router basics — the companion Phase 1 post, good if you haven't read it yet
- Next.js metadata and SEO — Open Graph, JSON-LD,
generateMetadata - Caching and revalidate — especially relevant for image-heavy content hubs that update frequently
Related on Baseline
- Next.js App Router basics — the companion post
- Next.js metadata and SEO — coming soon
- Next.js caching and revalidate — coming soon
- Case study: shipping a JSON formatter (performance thinking in practice)
- Baseline tools: JSON Formatter, JWT Decoder — free, no login
- Developers hub — more Next.js and React notes
Takeaways
next/imagehandles format selection, sizing, lazy-loading, and space reservation — use it for content images- One
priorityper page, on the hero/LCP image, and nowhere else fillneeds a positioned parent with real dimensions, plus asizeshint- Remote images must be allow-listed in
next.config next/fontis simpler and safer than the manual Google Fonts setup- Two font families max before you're hurting your own load time
If you remember only one thing: fix the hero image first — it's probably your LCP.