AI Reading
Quick summary of this article
Optimize a Next.js 16 app: next/image with correct sizes, next/font, code splitting with dynamic imports, bundle analysis, streaming, React Compiler and Partial Prerendering with cacheComponents.
next/imageonly helps when you provide a correctsizesattribute — without it the browser downloads the largest candidate.next/fontself-hosts fonts and eliminates the render-blocking request to Google Fonts.dynamic()splits heavy client-only libraries out of the initial bundle.cacheComponents: trueenables Partial Prerendering: a static shell with streamed dynamic holes.
Complete Next.js 16 Series — Module 11 of 12
Next.js does a lot of optimisation automatically, but the wins left on the table are almost always in the same five places: images, fonts, client bundle size, streaming and caching. This module covers each with measurable impact.
1. Images
next/image resizes, converts to AVIF/WebP and lazy-loads — but only if you tell it how wide the image will be rendered.
import Image from "next/image";
export function Hero() {
return (
<Image
src="/hero.webp"
alt="Product dashboard"
width={1600}
height={900}
priority
sizes="100vw"
className="h-auto w-full rounded-xl"
/>
);
}
Card images inside a grid need an accurate sizes, because the browser picks a candidate from srcset using it:
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{products.map((p) => (
<Image
key={p.id}
src={p.image}
alt={p.name}
width={480}
height={320}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="h-auto w-full rounded-lg"
/>
))}
</div>
Rules that matter:
priorityon the one above-the-fold image only. Marking everything priority defeats lazy loading.- Use
fillinside a positioned container for unknown dimensions. - Always set
sizes; omitting it makes the browser assume100vwand download oversized files.
Remote images need a config entry:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "images.example.com", pathname: "/products/**" },
],
qualities: [75, 90],
},
};
export default nextConfig;
Two Next.js 16 defaults changed: images.minimumCacheTTL is now 4 hours (was 60 seconds), and 16 was removed from the default imageSizes. The legacy images.domains option is deprecated — use remotePatterns with an explicit pathname.
2. Fonts
next/font downloads the font at build time, self-hosts it, and generates the @font-face rules with no external request:
// src/app/layout.tsx
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-sans",
});
const mono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${mono.variable}`}>
<body className="font-sans antialiased">{children}</body>
</html>
);
}
Then map it in globals.css:
@theme inline {
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
}
Result: no third-party DNS lookup, no layout shift, no cookie-consent implications.
3. Client Bundle Size
The build output is your primary metric. Watch the First Load JS column:
npm run build
Route (app) Size First Load JS
┌ ○ / 1.4 kB 112 kB
├ ○ /dashboard 3.1 kB 118 kB
└ ○ /reports 182 kB 297 kB ← investigate
/reports is 180 kB heavier. That is a charting library in the client bundle. Fix it with a dynamic import so it loads only when needed:
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("@/components/revenue-chart"), {
loading: () => <div className="h-72 animate-pulse rounded bg-neutral-100" />,
ssr: false,
});
ssr: false is correct for canvas-based charts that need window. Do not use it for content — it removes the HTML from the server render and hurts SEO.
Import only what you use:
// ❌ pulls the whole library
import { format } from "date-fns";
// ✅ per-function import tree-shakes reliably
import { format } from "date-fns/format";
Analyse deeper with the bundle analyzer:
npm install -D @next/bundle-analyzer
import withBundleAnalyzer from "@next/bundle-analyzer";
const nextConfig = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})({
// your config
});
export default nextConfig;
ANALYZE=true npm run build
4. Streaming and Suspense Boundaries
Streaming is the cheapest perceived-performance win available. Compare these two pages:
// ❌ everything waits for the slowest query
export default async function Page() {
const summary = await getSummary(); // 50ms
const feed = await getFeed(); // 900ms
return (
<>
<Summary data={summary} />
<Feed data={feed} />
</>
);
}
// ✅ shell and summary paint immediately, feed streams in
export default function Page() {
return (
<>
<Suspense fallback={<SummarySkeleton />}>
<SummarySection />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<FeedSection />
</Suspense>
</>
);
}
Place boundaries so each one resolves independently, and keep the fallback the same height as the real content to avoid layout shift.
5. Partial Prerendering with Cache Components
Enable it in next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
With Cache Components on, Next.js prerenders a static shell at build time and streams the dynamic parts into holes. The user gets instant HTML from the CDN, then personalised content arrives. The old experimental.ppr flag is removed in Next.js 16.
Mark the cacheable parts explicitly:
import { cacheLife, cacheTag } from "next/cache";
async function getStats() {
"use cache";
cacheLife("hours");
cacheTag("stats");
return db.stats.findMany();
}
Anything not marked use cache stays dynamic and streams. This is the endgame of Next.js caching: static where possible, dynamic where necessary, in the same page.
6. React Compiler
React Compiler 1.0 is stable and Next.js 16 supports it natively. It memoises components automatically, removing the need for most useMemo and useCallback:
npm install -D babel-plugin-react-compiler
const nextConfig: NextConfig = {
reactCompiler: true,
};
It is off by default because builds get slower (it runs through Babel). Turn it on for large, render-heavy client UIs; leave it off for small projects.
7. View Transitions
React 19.2 ships ViewTransition, and the App Router supports native page transitions without a library:
/* globals.css */
@view-transition {
navigation: auto;
}
Add view-transition-name to elements that should morph between routes — a product thumbnail becoming the hero image. The browser handles the animation off the main thread.
8. Measurements That Actually Matter
Optimise against numbers, not intuition:
- Build output — First Load JS per route, and static (
○) vs dynamic (ƒ) markers. - Lighthouse — LCP under 2.5s, CLS under 0.1, INP under 200ms.
- Server timing — add
export const dynamiconly after measuring; over-dynamicising kills caching. next buildwarnings — hydration mismatches and unused client boundaries show up here first.
A practical audit order: images with wrong sizes → fonts loaded from a CDN → heavy client library in a server tree → missing Suspense boundaries → uncached identical fetches.
Module 11 Recap
- Always supply
sizestonext/image;priorityonly for the single hero image. next/fontself-hosts fonts and removes render-blocking requests.- Dynamic-import heavy client-only libraries and watch First Load JS per route.
- Stream with
<Suspense>so slow sections never block the shell. cacheComponents: truegives a static shell with streamed dynamic holes.- React Compiler is stable but optional; measure build time before enabling.
- Audit in order: images, fonts, bundle, streaming, caching.
Next up — Module 12: testing, production deployment and the pre-launch checklist.
Continue the Course
- Course index: Next.js 16 Tutorial – all 12 modules
- Previous: Module 10: Authentication with Auth.js
- Next: Module 12: Testing & Deployment
