10 Next.js Performance Tips for Production Apps
Performance is part of the product. Next.js applications often begin fast, then slow down as client-side dependencies, large media, and uncertain caching accumulate. The solution is not a single configuration flag. It is a set of boundaries that keep unnecessary work away from the browser.
1. Default to Server Components
In the App Router, pages and layouts are Server Components by default. Keep them that way until a component actually needs browser APIs, local state, or event handlers.
The useful question is not “Can this be a Client Component?” It is “What is the smallest interactive leaf?” A product page can remain on the server while a favorite button or quantity picker becomes a focused Client Component.
export default async function ProductPage() { const product = await getProduct(); return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> <AddToCartButton productId={product.id} /> </article> ); }
This reduces the JavaScript that must be downloaded, parsed, and hydrated.
2. Give images accurate dimensions
Use next/image when its optimization behavior fits the source. Always provide dimensions or use fill inside a sized container, write meaningful alternative text, and include an accurate sizes value for responsive images.
import Image from "next/image"; export function Hero() { return ( <Image src="/hero.jpg" alt="Product dashboard overview" width={1440} height={900} sizes="(max-width: 768px) 100vw, 1200px" priority /> ); }
Reserve priority for the small number of images that genuinely affect the initial view. Loading every image eagerly defeats the point.
3. Lazy-load expensive interactive features
Charts, editors, maps, and media tools are common bundle offenders. If a feature is not needed at first paint, load it when its section becomes relevant.
import dynamic from "next/dynamic"; const ReportChart = dynamic(() => import("./ReportChart"), { loading: () => <ChartSkeleton />, ssr: false, });
Use ssr: false only for Client Components that depend on the browser. Dynamic import is a loading strategy, not a way to hide a component that should have rendered on the server.
4. Choose caching deliberately
Caching semantics have evolved across Next.js releases, so treat the versioned documentation as the source of truth. At the product level, the decision remains stable:
- cache content that can safely be reused;
- revalidate content that may be briefly stale;
- fetch per request when the response is private or must be current;
- invalidate cached data after a mutation that changes it.
Write that freshness requirement near the data access code. “Inventory can be sixty seconds old” is actionable. “Use caching” is not.
5. Keep client boundaries narrow
Adding "use client" makes that module part of the client dependency graph. A client boundary high in the tree can pull formatting libraries, data helpers, and large UI packages into the browser even when most of the page is static.
Keep data fetching and content rendering on the server. Pass serializable values into small interactive components. Periodically inspect imports above each client boundary; they often reveal accidental bundle growth.
6. Measure the bundle
Bundle analysis replaces intuition with evidence. Look for:
- a library imported for one small helper;
- duplicate versions of the same dependency;
- a feature loaded on routes where it never appears;
- icons or locale data imported as an entire package;
- client components that could move back to the server.
Measure a production build, not only the development server. Development mode optimizes for feedback and can hide the shape of the final assets.
7. Load fonts intentionally
next/font can self-host font files and reduce layout shift. Use only the families and weights the design needs. Every extra style adds bytes, and variable fonts are not automatically smaller for every use case.
import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], display: "swap", });
Match fallback metrics when visual stability matters, then verify the result on a throttled connection rather than assuming the configuration is enough.
8. Stream independent slow sections
One slow data source should not delay the whole route. Suspense boundaries allow the stable page shell to appear while slower regions continue loading.
import { Suspense } from "react"; export default function Page() { return ( <> <ProductSummary /> <Suspense fallback={<ReviewsSkeleton />}> <Reviews /> </Suspense> <Suspense fallback={<RecommendationsSkeleton />}> <Recommendations /> </Suspense> </> ); }
The fallback should preserve the approximate space and hierarchy of the arriving content. A good skeleton reduces perceived delay without creating a second layout.
9. Make third-party scripts earn their place
Analytics, chat widgets, video embeds, and A/B testing tools can dominate the main thread. Inventory every third-party script and record:
- who owns it;
- which routes need it;
- when it must load;
- and what user or business outcome justifies the cost.
Load non-critical scripts after the page becomes interactive, and remove tools that no longer have an owner. The fastest script is the one the browser never receives.
10. Monitor real users
Laboratory scores are useful, but production traffic includes slower devices, distant networks, extensions, and data the local fixture never represented. Track Core Web Vitals such as LCP, INP, and CLS by route and device class.
Pair the metrics with release markers. When a percentile moves after a deployment, the team should be able to connect the regression to a change. Define budgets before performance is bad enough to become an emergency.
A practical order of operations
Start with the page users visit most:
- inspect its client JavaScript;
- correct its largest images and fonts;
- confirm the freshness policy for each data source;
- delay non-essential interactive features;
- add real-user monitoring;
- repeat after the next meaningful feature lands.
Performance work succeeds when it becomes part of normal review, not an occasional rescue project. Keep the server/client boundary explicit, measure production output, and make every browser-side dependency justify its cost.
