Module 11: Next.js 16 Performance — Images, Fonts, Streaming & Cache Components

Module 11: Next.js 16 Performance — Images, Fonts, Streaming & Cache Components

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/image only helps when you provide a correct sizes attribute — without it the browser downloads the largest candidate.
  • next/font self-hosts fonts and eliminates the render-blocking request to Google Fonts.
  • dynamic() splits heavy client-only libraries out of the initial bundle.
  • cacheComponents: true enables 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:

  • priority on the one above-the-fold image only. Marking everything priority defeats lazy loading.
  • Use fill inside a positioned container for unknown dimensions.
  • Always set sizes; omitting it makes the browser assume 100vw and 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 dynamic only after measuring; over-dynamicising kills caching.
  • next build warnings — 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 sizes to next/image; priority only for the single hero image.
  • next/font self-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: true gives 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.

Leave a Reply

Your email address will not be published. Required fields are marked *