Concept
The beginner framing: Next.js ships dedicated components for the three assets that most commonly hurt page performance, images, fonts, and third-party scripts, each automatically applying the optimization best practices you'd otherwise have to implement by hand.
next/image: sized, modern-format, lazily-loaded images
import Image from "next/image";
<Image src="/profile.png" alt="Picture of the author" width={500} height={500} />next/image extends the HTML <img> element with: automatic serving of correctly-sized images per device in modern formats (WebP); automatic prevention of layout shift while loading, since the component knows the image's dimensions ahead of time; native browser lazy loading (only loading images as they approach the viewport) with optional blur-up placeholders; and on-demand resizing, even for remote images. A statically imported local image gets its width/height inferred automatically, you don't even need to specify them.
next/font: self-hosted, zero-layout-shift fonts
import { Geist } from "next/font/google";
const geist = Geist({ subsets: ["latin"] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
);
}next/font automatically self-hosts any font, including Google Fonts, as a static asset served from your own domain, meaning the browser never sends a request to Google's servers at all: a real privacy improvement (no third-party tracking request) and a real performance one (no separate DNS lookup/connection to an external font host). This built-in self-hosting is also what enables loading web fonts with no layout shift. Fonts are scoped to whatever component/file calls the font function, apply one globally by setting it on the root layout.
next/script: controlling exactly when a third-party script loads
import Script from "next/script";
<Script src="https://example.com/analytics.js" strategy="afterInteractive" />| Strategy | When it loads | Use for |
|---|---|---|
beforeInteractive | Before any Next.js code, before hydration | Critical scripts needed immediately (bot detection, consent management) |
afterInteractive (default) | Early, but after some hydration | Most analytics/tag-manager scripts |
lazyOnload | During browser idle time | Low-priority scripts (chat widgets, non-critical trackers) |
| () |
⚠️ Genuine gotcha, current as of this Next.js version: the
workerstrategy is not yet stable and does not yet work with the App Router. It's real, documented, and worth knowing about, but not something to reach for in an App Router project today.
A <Script> placed in a layout loads once for that layout and every nested route beneath it, and Next.js ensures it loads only once, even as the user navigates between routes sharing that layout, no re-fetching on every navigation within the same subtree.
Try It
Predict what happens before checking the solution.
<img src="/hero.jpg" alt="Hero" /> {/* plain <img>, no width/height */}
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} /> {/* next/image */}Which one is more likely to cause a layout shift while the page loads?
Solution
The plain <img> tag. Without explicit dimensions, the browser doesn't know how much space to reserve for the image before it finishes loading, once it loads, the surrounding content jumps to make room, a real Cumulative Layout Shift (CLS) event. next/image, given explicit width/height (or inferred dimensions from a static import), reserves the correct space immediately, so nothing shifts once the image finishes loading.
Implement It Yourself
Model the layout-shift-prevention logic next/image applies automatically:
function computeReservedSpace({ width, height, staticImport }) {
if (staticImport) {
return { width: staticImport.width, height: staticImport.height, source: "inferred from the imported file" };
}
if (width && height) {
return { width, height, source: "explicit props" };
}
return { width: null, height: null, source: "⚠️ UNKNOWN, will cause layout shift when the image loads" };
}
computeReservedSpace({ width: 500, height: 500 });
// { width: 500, height: 500, source: "explicit props" }, space reserved immediately
computeReservedSpace({});
// { width: null, height: null, source: "⚠️ UNKNOWN..." }, exactly the plain <img> problemThis is the essential mechanism: knowing the image's dimensions before it loads is what lets the browser reserve the right amount of space up front, eliminating the shift entirely.
Under the Hood
All three optimizations are attacking the same underlying browser-rendering concepts covered generally in Performance, Cumulative Layout Shift (a Core Web Vital) for images and fonts, and main-thread blocking time for scripts. next/image and next/font don't introduce new browser capabilities; they apply the same manual best practices (explicit dimensions, self-hosted assets with matched fallback metrics) a careful developer would otherwise implement by hand, automatically and by default.
Common Mistakes
1. Using a plain <img> tag instead of next/image
Forfeits automatic sizing, format optimization, lazy loading, and layout-shift prevention, all four benefits require the dedicated component.
2. Reaching for strategy="worker" in an App Router project today
Covered above, this strategy is explicitly documented as not yet stable and not yet functional in the App Router specifically. Reaching for it now produces broken or unsupported behavior, not a working optimization.
3. Loading a third-party script with the default afterInteractive when it's actually needed before hydration
Some scripts (certain consent-management or bot-detection tools) genuinely need to run before the page becomes interactive, leaving them at the default afterInteractive can cause them to load too late for their intended purpose. Use beforeInteractive deliberately for these, not as a default.
4. Not applying next/font's className to the root layout for a site-wide font
Since fonts are scoped to wherever the font function is called, forgetting to apply the resulting className at the root layout level means the font only applies to whatever narrower scope it was actually set on.
Best Practices
- Always use
next/imagefor content images, providing explicit dimensions (or a static import) so layout shift prevention actually works. - Use
next/fontfor any web font, including Google Fonts, to get automatic self-hosting, privacy, and zero-layout-shift benefits for free. - Choose a
next/scriptstrategy deliberately based on the script's actual urgency, don't leave everything at the default without considering whether it's truly needed before or after hydration. - Avoid the
workerstrategy in App Router projects until it's stabilized and supported there.
Performance Tips
- Layout shift (CLS) is a scored Core Web Vital,
next/image's automatic dimension reservation andnext/font's zero-shift loading directly and measurably improve this score, not just "feel" faster. - Third-party scripts are one of the most common sources of main-thread blocking time, deliberately choosing
lazyOnloadfor genuinely low-priority scripts (chat widgets, non-critical trackers) keeps the main thread free for the content that actually matters to the user first. next/font's self-hosting eliminates an entire external network round-trip (DNS + connection + download from a font CDN) that a<link>-based Google Fonts setup would otherwise require.
