Next.js Rendering Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Default to Server Components: Every component in the App Router is a Server Component unless you opt out with
"use client", so reach for Server Components first and only add the directive when you actually need hooks, events, or browser APIs. - Await Directly in Async Components: Server Components can be
asyncand callawait fetch(...)orawait db.query(...)in the function body — skipuseEffectloading states, since hooks are not allowed on the server anyway. - Serialize Props Across the Boundary: Props passed from a Server Component to a Client Component must be JSON-serializable (strings, numbers, plain objects, arrays, Server Actions); regular functions, class instances, Refs, and Symbols fail silently or throw.
- Use server-only for Secrets:
import "server-only"at the top of modules that readAUTH_SECRET, database URLs, or signing keys makes the build fail the moment a Client Component imports them, preventing leakage into the browser bundle. - Push use client Boundaries Low: Marking a top-level component as
"use client"pulls every child and every imported utility into the client bundle; extract only the interactive leaf (like aLikeButton) into the Client Component and keep its parents server-rendered. - Guard Browser APIs in useEffect:
window,document,localStorage, andIntersectionObserverdo not exist during SSR, so access them insideuseEffect(or behind atypeof window !== "undefined"check) and initialize state to a server-safe value first. - Avoid Hydration Mismatches: Never call
Date.now(),Math.random(), or readwindowduring render — the server HTML and client re-render will disagree; move varying values intouseEffect, or usesuppressHydrationWarningfor deliberate cases. - Pass Server Components as children: A Client Component cannot
importa Server Component (the import silently becomes client-only), but it can receive one throughchildrenor named JSX props — orchestrate the composition from a Server Component parent. - Isolate Providers in a "use client" File: React Context requires a Client Component, but putting providers in the root layout would client-ify everything; instead, put them in
app/providers.tsxwith"use client"and render<Providers>{children}</Providers>from the Server Component layout. - Extract Only the Interactive Piece: Instead of converting a whole card into a Client Component for one button, keep the card as a Server Component and extract the button (e.g.,
LikeButton) into its own small"use client"file. - Know What Triggers Dynamic:
cookies(),headers(),searchParams,connection(), anycache: "no-store"fetch, orexport const dynamic = "force-dynamic"opt the whole route into dynamic rendering — a single use in any component is enough. - Use generateStaticParams for Known Paths: Pre-render every path you can list at build time by returning its params from
generateStaticParams, and leavedynamicParams = true(the default) so new paths are rendered on first request and then cached. - revalidate: 0 Means Always Dynamic:
export const revalidate = 0is equivalent toforce-dynamic, not "revalidate immediately" — use a positive integer (e.g.,revalidate = 60) for ISR and reserve0or"no-store"for truly per-request data. - Don't Mix force-static With Dynamic Functions:
export const dynamic = "force-static"makes the build fail if the page callscookies()/headers()/searchParams; either remove the dynamic call or drop back todynamic = "auto". - Enable PPR Incrementally: Set
experimental: { ppr: "incremental" }innext.config.tsand opt routes in one at a time withexport const experimental_ppr = trueso you can ship PPR progressively and verify each route behaves. - Wrap Dynamic Children in Suspense: Suspense boundaries define the static-shell-vs-dynamic-hole split; a dynamic component without a surrounding
<Suspense>pulls the entire PPR route back into full dynamic rendering. - Consolidate Dynamic Holes: Each Suspense hole costs server compute at request time, so group related request-time data into fewer boundaries rather than scattering a dozen tiny ones and negating the CDN shell.
- Always Provide sizes With fill: An
<Image fill>without asizesprop makes the browser request the largest variant on every device; supply a breakpoint-aware string like"(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"that matches the layout. - Configure remotePatterns: Any external image host needs an entry in
images.remotePatternsinnext.config.ts; without it, remote images throw a build/runtime error andnext/imagerefuses to optimize them. - Reserve priority for LCP Images: Add
priorityonly to the one or two above-the-fold LCP images — it disables lazy loading and injects a preload hint, so overusing it slows the page instead of speeding it up. - width/height Sets Aspect Ratio, Not Size: The
widthandheightprops onnext/imagelock in an aspect ratio for CLS prevention; use CSS (className, wrapper sizing) to control what the image actually renders at. - Use next/font for Self-Hosting:
next/font/googleandnext/font/localdownload and serve fonts from your own origin with immutable cache headers, eliminating external requests, FOUT, and CLS via auto-generated fallback metrics. - Apply className or variable to the DOM: Fonts only activate when you attach
inter.className(orinter.variable) to<html>,<body>, or the relevant container; a forgotten class is the most common "my font isn't loading" bug. - Omit weight for Variable Fonts: Passing explicit
weight: ["400", "700"]for a font that has a variable build forces Next.js to download multiple static files instead of one variable file; dropweightwhen the font supports it to keep the bundle small. - Prefer display: swap: Use
display: "swap"(the recommended default) so a fallback font shows immediately and is replaced when the custom font loads;display: "optional"can leave text invisible if the font does not arrive within ~100ms.