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/image with correct dimensions or a sized fill container
  • priority only on the actual LCP image
  • next/font for 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 dimensions
  • sizes — 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 responsive
  • priority → only the main hero / LCP image, once per page
  • remotePatterns → any image URL not served from your public/ folder
  • next/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/height and getting a jumpy layout
  • Marking every hero-area image as priority (defeats the purpose)
  • Missing sizes on 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 remotePatterns in next.config.ts
  • fill image not visible — the parent element needs position: relative and 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 html element and you're actually using var(--font-sans) in your CSS
  • LCP still slow — one priority max, compress the source file before upload, check how much JS is loading on that page

Checklist before you ship

  • [ ] Hero uses next/image with real dimensions or a sized fill container
  • [ ] Only the actual LCP image has priority
  • [ ] Responsive fill images have a sizes hint
  • [ ] 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:

  1. Add a hero image with priority on the home page
  2. Build a 3-card grid where each card uses fill + a sensible sizes hint
  3. Load Inter for UI text and one serif font for article body via next/font
  4. Apply serif to a .article-body class, sans to everything else
  5. 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

  1. Next.js App Router basics — the companion Phase 1 post, good if you haven't read it yet
  2. Next.js metadata and SEO — Open Graph, JSON-LD, generateMetadata
  3. Caching and revalidate — especially relevant for image-heavy content hubs that update frequently
  • 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/image handles format selection, sizing, lazy-loading, and space reservation — use it for content images
  • One priority per page, on the hero/LCP image, and nowhere else
  • fill needs a positioned parent with real dimensions, plus a sizes hint
  • Remote images must be allow-listed in next.config
  • next/font is 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.