Type something to search...
Next.js Production checklist

Next.js Production checklist

Every article in this series so far has covered one specific feature in depth. This one is different on purpose — it's the checklist you actually run through in the weeks before a Next.js app goes live, pulling together decisions that are easy to make correctly by default and only slightly harder to get wrong by accident. None of this is exotic; it's mostly a matter of knowing what Next.js already gives you for free, and what still needs a deliberate choice from you.

What you get automatically, with zero configuration

Worth starting here because it's easy to under-appreciate how much of the "production readiness" work is already done before you write a line of optimization code.

Server Components are the default. They run entirely on the server and contribute nothing to your client-side JavaScript bundle — you reach for Client Components deliberately, for interactivity, rather than by default.

Code-splitting happens per route automatically. Server Components make this possible without you configuring bundle splits manually; you can go further by lazy-loading specific Client Components or heavy third-party libraries where it matters.

Prefetching happens as links enter the viewport. Next.js loads a route's resources in the background before the click, which is most of why navigation in a Next.js app feels instant by default rather than something you had to engineer.

Prerendering happens at build time wherever possible. Both Server and Client Component output gets prerendered and cached where it can be, with Dynamic Rendering available as an explicit opt-in for routes that genuinely need per-request freshness.

Caching happens across data requests, rendered output, and static assets, reducing round trips to your server, database, and backend services — again, with an explicit opt-out available for the routes where fresh-every-time is actually correct behavior.

None of this needs configuration to get. It needs configuration to deviate from when the default doesn't fit a specific route.

During development: routing and rendering

Use layouts deliberately, not just for shared UI, but because they're what enables partial rendering on navigation — a layout that doesn't change between two routes doesn't need to re-render when navigating between them.

Use <Link>, not raw anchor tags, for anything that should get client-side navigation and automatic prefetching — this is the single most common thing to accidentally regress by reaching for a plain <a> out of habit.

Build real error handling before you need it. A custom error.tsx for catch-all errors and a proper 404 experience aren't nice-to-haves you add after launch — they're the difference between a broken route showing your users a stack trace and showing them something coherent.

Be deliberate about "use client" boundary placement. Following the recommended Server/Client composition patterns, and actually checking where your client boundaries sit, is what prevents a client bundle from silently absorbing far more than it needs to.

Treat request-time APIs as a decision, not a habit. cookies() and the searchParams prop both opt an entire route into Dynamic Rendering the moment you use them — and if used in the Root Layout specifically, that opts your entire application into it. Using either should be a conscious choice, wrapped in <Suspense> where appropriate, not something that happens incidentally because a utility function you imported happened to read a cookie.

During development: data fetching and caching

Fetch data in Server Components to get the framework's data-fetching benefits by default, rather than defaulting to client-side fetching out of old habits from a pre-App-Router mental model.

Don't call Route Handlers from Server Components. Route Handlers exist so Client Components can reach your backend resources — calling one from a Server Component just adds an unnecessary network hop for data you could have fetched directly.

Use Suspense and loading UI to avoid blocking the whole route. Streaming lets the framework send UI progressively rather than waiting for every last data dependency to resolve before anything reaches the browser.

Fetch in parallel wherever the data doesn't actually depend on itself sequentially. Sequential awaits that don't need to be sequential are the most common self-inflicted network waterfall in App Router code, and they're almost always a straightforward fix once spotted.

Actually verify your caching is doing what you think it's doing. Check whether your data requests are cached as intended, and — a detail that's easy to overlook — make sure requests that don't go through fetch (a direct database client call, say) are wrapped so they're cached too; caching isn't automatic for arbitrary data-access code the way it is for fetch.

Put static assets in public/ so they're automatically cached rather than treated as something that needs its own caching strategy.

During development: UI and accessibility

Handle forms with Server Actions, including server-side validation and error handling, rather than hand-rolling a client-side fetch-and-validate flow that duplicates what the framework already does well.

Add both a global error boundary and a global 404. app/global-error.tsx and app/global-not-found.tsx give you consistent, accessible fallback UI across the entire app, rather than leaving unmatched routes and uncaught exceptions to produce inconsistent experiences depending on which part of the app they happened in.

Use the built-in optimization components rather than raw HTML equivalents. The Font Module self-hosts fonts and removes external network requests while reducing layout shift; the <Image> component optimizes images automatically and serves modern formats like WebP while preventing layout shift; the <Script> component defers third-party scripts so they don't block the main thread. All three exist specifically because the naive HTML equivalent (a plain <img>, an unmanaged <script src>) tends to actively hurt performance in ways that aren't obvious until you measure them.

Lint for accessibility, don't just eyeball it. The built-in eslint-plugin-jsx-a11y plugin catches a meaningful class of accessibility issues automatically, before they ship.

During development: security

Taint sensitive data objects so they can't accidentally leak to the client — this is a Next.js-specific mechanism worth knowing about even if you've never needed it before, precisely because the failure mode it prevents (a sensitive field silently ending up in client-rendered output) tends to be invisible until someone notices it in production.

Never trust a Server Action's caller without checking. Verify authentication and authorization inside every action itself — don't rely on Proxy-level or layout/page-level checks alone, since a Server Action can in principle be invoked directly, bypassing whatever UI gating you assumed was the only path to it. Move database access into a server-only-marked Data Access Layer, and consider rate limiting for anything expensive to run.

Keep .env* files out of version control, and audit that only variables genuinely meant for the browser carry the NEXT_PUBLIC_ prefix — a variable prefixed this way gets inlined into the client bundle at build time, which is not reversible after the fact.

Consider a Content Security Policy. It's real, ongoing work to configure correctly, but it's a meaningful layer of defense against cross-site scripting, clickjacking, and code injection specifically.

During development: metadata, SEO, and type safety

Use the Metadata API for page titles, descriptions, and the rest of what search engines and social platforms actually read — this is genuinely a five-minute win per page compared to hand-rolling <head> tags.

Generate Open Graph images so links to your pages look intentional when shared on social platforms, rather than showing a generic placeholder or nothing at all.

Generate a sitemap and a robots file so search engines can actually discover and correctly crawl your pages, rather than relying entirely on incidental link discovery.

Use TypeScript, with the TypeScript plugin enabled. This isn't Next.js-specific advice so much as a blanket recommendation that pays for itself many times over in a codebase of any real size — catching a class of bugs at compile time rather than in production is almost always worth the friction.

Before going to production: measure, don't guess

Run next build locally to catch build errors before they surface in a deploy pipeline, then next start to measure real production-mode performance — dev mode's performance characteristics are meaningfully different from production and shouldn't be your reference point for anything resembling a load-time judgment call.

Run Lighthouse, in incognito. Incognito mode avoids browser extensions skewing the results, and gives you a reasonably faithful simulation of what an actual visitor experiences. Treat it as one input, not the whole picture — it's a simulated test, and it should be paired with real field data (actual Core Web Vitals from actual users) rather than trusted in isolation, since a synthetic Lighthouse run and real-world device/network diversity can diverge meaningfully.

Wire up useReportWebVitals to send Core Web Vitals data to whatever analytics tool you're using — this is what turns "we ran Lighthouse once before launch" into an ongoing signal you can actually watch over time as the app evolves.

Analyze your bundles before shipping, not after users complain. The @next/bundle-analyzer plugin (for webpack) surfaces large modules and dependencies that might be quietly weighing your app down. Complementary tools worth having in your workflow for evaluating whether a new dependency is worth its cost before you even add it: Import Cost (an editor extension showing bundle impact inline), Package Phobia, Bundle Phobia, and bundlejs — all answer some version of "how much does this actually cost me" before you commit to a library.

Key Takeaways

CategoryWhat to actually check
Automatic optimizationsConfirm you understand what's free by default before "optimizing" something that already is
Routing/renderingLayouts used deliberately, <Link> over raw <a>, "use client" boundaries audited
Data fetchingServer Components for fetching, parallel where possible, non-fetch requests explicitly cached
UI/accessibilityGlobal error + 404 pages, Font/Image/Script components used, jsx-a11y linting on
SecurityServer Actions verify auth internally, .env* gitignored, NEXT_PUBLIC_ audited, CSP considered
SEOMetadata API, OG images, sitemap + robots
Pre-launch measurementnext build && next start, Lighthouse in incognito, useReportWebVitals wired up, bundle analyzed

None of these items are individually hard — the value of treating this as an actual checklist, run through deliberately before launch, is that it's exactly the kind of list that's easy to think you've covered from memory and easy to actually miss two or three items from in practice. Running through it explicitly, once, before shipping, is cheap insurance against the class of production issue that's obvious in hindsight and invisible until a user hits it.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper