A Practical Next.js 16 Production Checklist for App Router
Shipping a Next.js 16 App Router project on Vercel? Our production checklist covers Server vs Client Components, new caching, ISR, and common deployment issues.
The Next.js App Router is a significant shift from the Pages Router. It introduces powerful features like Server Components and granular caching, but these also bring new complexities. Getting an app from local development to a stable production environment on Vercel requires a new set of checks and considerations.
At JRV Systems, we build complex systems for Malaysian businesses, from e-commerce platforms to clinic management SaaS. We've guided several clients through this transition. This article is our distilled, practical Next.js 16 production checklist, focusing on what truly matters before you go live.
Your Next.js 16 Production Checklist
Before deploying, run through these core areas. They represent the most common points of failure or performance degradation we've seen when moving App Router projects to production.
- Component Strategy: Have you explicitly decided which components are Server Components and which are Client Components? The default is Server.
- Caching and Data Fetching: Are you intentionally using the new
fetchcaching, or have you opted out where necessary? Static pages can become unintentionally dynamic. - Revalidation Logic: If you're using Incremental Static Regeneration (ISR), is your on-demand revalidation webhook or path-based logic correctly configured and secured?
- Environment Variables: Are all server-side and client-side variables correctly configured in your Vercel project settings? Remember the
NEXT_PUBLIC_prefix for client-side variables. - Error Handling and Logging: Have you set up
error.jsandglobal-error.jsfiles to handle runtime errors gracefully? Is a logging service like Vercel Logs or a third-party tool integrated? - Bundle Size Analysis: Have you used
@next/bundle-analyzerto check for unexpectedly large client-side JavaScript bundles? Large Client Components can slow down your site.
Server vs. Client Components: The Core Decision
The App Router is server-first. Every component is a React Server Component (RSC) by default. This is great for performance, as it reduces the amount of JavaScript sent to the browser. However, any interactivity requires a Client Component.
The rule is simple: if your component uses hooks like useState, useEffect, or useContext, or relies on browser-only APIs and event listeners (onClick, onChange), you must mark it with the "use client" directive at the top of the file.
A common mistake is making large layout components client-side when only a small part of them is interactive. The best practice is to keep components on the server as much as possible. Isolate interactivity into smaller, specific Client Components. For example, a product page can be a Server Component that fetches data, while the 'Add to Cart' button within it is a small, self-contained Client Component.
Understanding the New Caching Defaults
This is one of the biggest changes from the Pages Router. In the App Router, calls to fetch are automatically cached indefinitely by default. This is extremely powerful for static content but can cause major headaches if you expect fresh data.
If you fetch data that changes, you must specify a caching strategy:
-
Time-based Revalidation (ISR): To fetch new data periodically, use the
next.revalidateoption. This tells Next.js to cache the result for a specific number of seconds.fetch('https://api.example.com/data', { next: { revalidate: 3600 } })// Revalidates every hour -
Opting Out of Caching: For data that must always be fresh (e.g., user session information, stock levels), you must explicitly disable caching.
fetch('https://api.example.com/data', { cache: 'no-store' })
Forgetting to set cache: 'no-store' for dynamic data is a frequent source of bugs we've debugged for clients launching new e-commerce sites. The product prices or stock levels appear stale until the next deployment clears the cache.
Implementing On-Demand Revalidation
Time-based revalidation is useful, but often you want to update a page instantly when its data changes. This is 'on-demand' ISR. For example, when a clinic updates a doctor's schedule in their SaaS dashboard, you want the public-facing schedule page to reflect that change immediately.
This is achieved using two main approaches:
- Tag-based Revalidation: You can 'tag' specific
fetchrequests with a string. Later, you can call an API route or Server Action that uses therevalidateTag('tag-name')function to invalidate all fetches associated with that tag. - Path-based Revalidation: If you want to revalidate a specific URL path, you can use
revalidatePath('/your-page-slug').
To secure this, the revalidation endpoint is typically a webhook protected by a secret token. Your CMS or backend system calls this webhook after data is updated, passing the secret token to trigger the revalidation on your Next.js application.
4 Common Production Issues We've Debugged
Based on our work at JRV Systems, these four issues consistently appear when teams ship their first App Router project.
-
Mismatched Node.js Versions: A project works perfectly on a developer's machine running Node.js 20 but fails on Vercel, which might be targeting Node.js 18. Always specify the exact Node.js version in your
package.jsonenginesfield and configure it in your Vercel project settings to ensure consistency. -
Improper Environment Variable Handling: A developer forgets to add
NEXT_PUBLIC_to an API key that needs to be accessed in the browser. On Vercel, this variable will beundefinedon the client, breaking features. A clear separation between server (process.env.DB_PASSWORD) and client (process.env.NEXT_PUBLIC_GA_ID) variables is crucial. -
Unintentional Dynamic Rendering: A page intended to be static (
SSG) is rendered dynamically on every request. This often happens when a dynamic function likecookies()orheaders()is used in a component or layout that affects the page. This kills performance and increases costs. Use Vercel's build logs to check which pages are rendered statically versus dynamically. -
Database Connection Exhaustion: In a serverless environment, each request can spin up a new function instance, potentially creating a new database connection. Without proper connection pooling (using a service like Neon or Supabase's PgBouncer), you can quickly exhaust your database's connection limit. This is particularly critical for applications with spiky traffic, like billing systems or promotional e-commerce pages.