Chapter 10 — Image & Font Optimization

Chapter 10 — Image & Font Optimization

Hey everyone! Two of the biggest, most boring-to-implement-yourself performance wins in web development: correctly sized/lazy-loaded images, and fonts that don't cause layout shift. Next.js bakes both in as near-drop-in components. Let's see what they actually do under the hood — because "just use next/image" without understanding why is how bugs happen.

What we will cover:

  • The problems with a plain <img> tag
  • next/image — what it automates for you
  • Required props and common mistakes
  • The problem with web fonts, and how next/font fixes it
  • Using Google Fonts and local fonts

1. The Problems With Plain <img>

<img src="/hero.jpg" />

Problems this quietly causes:
================================
✘ Sends the SAME full-size image to phones and 4K monitors alike
✘ No lazy-loading — images below the fold load immediately, wasting bandwidth
✘ No modern format conversion (still shipping .jpg when .webp is smaller)
✘ Browser doesn't know the image's size until it loads → page JUMPS
  around as images pop in (this is "Cumulative Layout Shift", a real
  Google ranking factor)

2. next/image — What It Automates

import Image from "next/image";

<Image
    src="/hero.jpg"
    alt="Hero banner"
    width={1200}
    height={600}
/>
What happens automatically behind this ONE component:
=========================================================
✔ Resizes the image to fit the ACTUAL rendered size on each device
✔ Serves modern formats (WebP/AVIF) when the browser supports them
✔ Lazy-loads by default — images off-screen don't load until you scroll near them
✔ Reserves the exact space (width/height) BEFORE the image loads
  → zero layout shift, page doesn't jump around
✔ Generates a blurred placeholder automatically, if you ask for it

3. Required Props & Common Mistakes

❌ MISTAKE 1 — forgetting width/height:
==========================================
<Image src="/hero.jpg" alt="Hero" />
// Error! next/image REQUIRES width+height (or "fill") so it can
// reserve space and prevent layout shift. This isn't optional.

✅ FIX — either give explicit dimensions, or use "fill" for a
   container-sized image:
<div style={{ position: "relative", width: "100%", height: "400px" }}>
    <Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: "cover" }} />
</div>

❌ MISTAKE 2 — using next/image with a REMOTE (external) image URL
   without configuring it:
==========================================================================
<Image src="https://cdn.example.com/photo.jpg" ... />
// Error! Next.js blocks unknown external hosts by default (security).

✅ FIX — allow the domain in next.config.js:
// next.config.js
module.exports = {
    images: {
        remotePatterns: [{ hostname: "cdn.example.com" }],
    },
};

4. Priority Loading for Above-the-Fold Images

<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />

Without "priority": your hero/banner image is lazy-loaded like
everything else — meaning it might load LATE, hurting your
"Largest Contentful Paint" score (a real performance metric).

With "priority": Next.js preloads it immediately, no lazy-loading delay.
Use this ONLY for the image visible without scrolling — using it
everywhere defeats the whole purpose of lazy-loading.

5. The Problem With Web Fonts

Traditional web font loading (Google Fonts <link> tag):
============================================================
1. Browser downloads your HTML/CSS
2. Browser requests the font from fonts.googleapis.com (a THIRD PARTY!)
3. Text renders with a FALLBACK font while waiting
4. Font arrives → text suddenly RESHUFFLES into the real font
   ("Flash of Unstyled Text" / layout shift, again)

Also: that third-party request is a real privacy/performance cost —
you're making every visitor's browser talk to Google's servers.

6. next/font — Self-Hosted, Zero Layout Shift

// app/layout.js
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({ children }) {
    return (
        <html lang="en" className={inter.className}>
            <body>{children}</body>
        </html>
    );
}
What next/font does differently:
====================================
✔ DOWNLOADS the font file at BUILD time and self-hosts it with your app
  → no request to Google's servers at all, at runtime
✔ Automatically calculates fallback font metrics to MATCH the real
  font's size → no visible reshuffle when the real font loads
✔ Works identically for Google Fonts AND your own local font files
Local (self-owned) fonts:
============================
import localFont from "next/font/local";

const myFont = localFont({ src: "./my-custom-font.woff2" });

Key Points to Remember

  • Plain <img> ships oversized images and causes layout shift — next/image fixes both automatically
  • next/image requires width+height (or fill) — this is what enables zero layout shift, not a limitation to work around
  • Remote image domains must be allow-listed in next.config.js — a deliberate security default
  • Use priority only on the one image visible without scrolling (your LCP element)
  • next/font self-hosts fonts at build time — no third-party request, no flash of unstyled text
  • Both tools exist because "just use a normal tag" quietly costs you real Core Web Vitals score

What's Next?

We've now covered the core surface area of Next.js. The next two chapters go deeper under the hood — how rendering and caching ACTUALLY work internally, past the "just trust the framework" level.

Keep coding, keep learning! See you in the next one!