React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummarynextjs-rendering

Next.js Rendering Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. 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.
  2. Await Directly in Async Components: Server Components can be async and call await fetch(...) or await db.query(...) in the function body — skip useEffect loading states, since hooks are not allowed on the server anyway.
  3. 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.
  4. Use server-only for Secrets: import "server-only" at the top of modules that read AUTH_SECRET, database URLs, or signing keys makes the build fail the moment a Client Component imports them, preventing leakage into the browser bundle.
  5. 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 a LikeButton) into the Client Component and keep its parents server-rendered.
  6. Guard Browser APIs in useEffect: window, document, localStorage, and IntersectionObserver do not exist during SSR, so access them inside useEffect (or behind a typeof window !== "undefined" check) and initialize state to a server-safe value first.
  7. Avoid Hydration Mismatches: Never call Date.now(), Math.random(), or read window during render — the server HTML and client re-render will disagree; move varying values into useEffect, or use suppressHydrationWarning for deliberate cases.
  8. Pass Server Components as children: A Client Component cannot import a Server Component (the import silently becomes client-only), but it can receive one through children or named JSX props — orchestrate the composition from a Server Component parent.
  9. 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.tsx with "use client" and render <Providers>{children}</Providers> from the Server Component layout.
  10. 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.
  11. Know What Triggers Dynamic: cookies(), headers(), searchParams, connection(), any cache: "no-store" fetch, or export const dynamic = "force-dynamic" opt the whole route into dynamic rendering — a single use in any component is enough.
  12. Use generateStaticParams for Known Paths: Pre-render every path you can list at build time by returning its params from generateStaticParams, and leave dynamicParams = true (the default) so new paths are rendered on first request and then cached.
  13. revalidate: 0 Means Always Dynamic: export const revalidate = 0 is equivalent to force-dynamic, not "revalidate immediately" — use a positive integer (e.g., revalidate = 60) for ISR and reserve 0 or "no-store" for truly per-request data.
  14. Don't Mix force-static With Dynamic Functions: export const dynamic = "force-static" makes the build fail if the page calls cookies()/headers()/searchParams; either remove the dynamic call or drop back to dynamic = "auto".
  15. Enable PPR Incrementally: Set experimental: { ppr: "incremental" } in next.config.ts and opt routes in one at a time with export const experimental_ppr = true so you can ship PPR progressively and verify each route behaves.
  16. 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.
  17. 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.
  18. Always Provide sizes With fill: An <Image fill> without a sizes prop 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.
  19. Configure remotePatterns: Any external image host needs an entry in images.remotePatterns in next.config.ts; without it, remote images throw a build/runtime error and next/image refuses to optimize them.
  20. Reserve priority for LCP Images: Add priority only 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.
  21. width/height Sets Aspect Ratio, Not Size: The width and height props on next/image lock in an aspect ratio for CLS prevention; use CSS (className, wrapper sizing) to control what the image actually renders at.
  22. Use next/font for Self-Hosting: next/font/google and next/font/local download and serve fonts from your own origin with immutable cache headers, eliminating external requests, FOUT, and CLS via auto-generated fallback metrics.
  23. Apply className or variable to the DOM: Fonts only activate when you attach inter.className (or inter.variable) to <html>, <body>, or the relevant container; a forgotten class is the most common "my font isn't loading" bug.
  24. 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; drop weight when the font supports it to keep the bundle small.
  25. 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.