A Practical Next.js 16 Production Checklist for Vercel Deployments
Shipping a Next.js 16 app with the App Router? Our production checklist covers Server Components, new caching defaults, ISR, and the 4 key issues we've fixed.
The Next.js App Router is a significant shift from the Pages Router. While powerful, its new paradigms—especially Server Components and data fetching—introduce new potential issues in production. At JRV Systems, we've migrated several client projects, from e-commerce sites to internal dashboards, to the App Router. This experience has helped us build a reliable pre-deployment process.
This article is our internal Next.js 16 production checklist, refined from real-world debugging sessions. It focuses on the most common and impactful areas we've seen cause problems after going live on Vercel.
Server vs. Client Components: When to Use Which
The App Router's most fundamental change is its 'server by default' model. Understanding this is the first step to building an efficient application.
Server Components (The Default)
These components run exclusively on the server. They cannot use hooks like useState or useEffect and cannot handle browser events like onClick.
Use them for:
- Direct data fetching (e.g., accessing a database or a private API).
- Keeping large dependencies out of the client-side JavaScript bundle. For example, a heavy data processing or date formatting library can stay on the server.
- Accessing server-only resources like environment variables or file systems.
Client Components (Opt-in with 'use client')
To make a component interactive, you must add the 'use client' directive at the top of the file. This marks it and all its child components as client-side code.
Use them for:
- Handling user interactions (
onClick,onChange, etc.). - Managing state with hooks like
useState,useReducer, anduseContext. - Using browser-only APIs such as
localStorage,window, or the Geolocation API.
A practical rule we follow: start every component as a Server Component. Only add 'use client' when you absolutely need interactivity or a browser-specific API. Keep your Client Components as small and specific as possible—think of them as interactive islands within a static, server-rendered page.
Understanding the New Caching Defaults
In the Pages Router, you had clear boundaries with getServerSideProps (dynamic) and getStaticProps (static). The App Router unifies this within the native fetch API, but its default behavior is a common source of confusion.
By default, any fetch request in a Server Component is automatically cached indefinitely. This is similar to getStaticProps and can lead to users seeing stale data if you're not careful.
To control this behavior, you use options within the fetch call:
-
Dynamic Data (like
getServerSideProps): To ensure data is fetched on every request, use thecache: 'no-store'option. This is essential for user-specific dashboards or real-time information.fetch('https://api.example.com/data', { cache: 'no-store' }); -
Revalidated Data (like ISR): To cache data for a specific period, use the
next.revalidateoption. The value is in seconds. This is great for content that updates periodically but not constantly, like a blog or news feed.fetch('https://api.example.com/news', { next: { revalidate: 3600 } }); // Revalidate every hour
Forgetting to set the correct cache policy is the most frequent issue we debug for clients moving to the App Router. Always be explicit about your data's freshness requirements.
On-Demand Revalidation for Dynamic Content
Incremental Static Regeneration (ISR) is powerful, but waiting for a timer to expire isn't always ideal. On-demand revalidation lets you manually purge the cache for a specific page or data tag, providing instant updates.
This is typically done via a secure API route triggered by a webhook from a Headless CMS or backend system. For a Malaysian e-commerce client, we implemented a system where their inventory management tool calls a webhook whenever stock levels change. This webhook triggers a Next.js API route that executes revalidatePath:
revalidatePath('/products/[slug]', 'page')
This immediately rebuilds the specific product page with the new stock information. The user sees the update instantly without a full site deployment. You can also use revalidateTag to invalidate multiple pages that share the same data tag, which is highly efficient.
Four Common Production Issues We've Debugged
Beyond caching, a few other issues regularly appear in production environments.
-
Hydration Errors: This happens when the HTML rendered on the server doesn't match what React renders on the client. The most common cause is using a browser-only API (like
window.innerWidth) in a component that isn't marked with'use client', or conditionally rendering content based on a value that only exists after the page loads in the browser. -
Incorrect Environment Variable Exposure: A classic mistake. Only environment variables prefixed with
NEXT_PUBLIC_are available in browser-facing code (Client Components). Server-side keys likeDATABASE_URLorSTRIPE_SECRET_KEYshould never have this prefix. Accessing a non-prefixed variable in a Client Component will result in it beingundefined, leading to runtime errors. -
Large Client Bundles: It's easy to accidentally import a large library into a Client Component, bloating the JavaScript sent to the user. We always recommend using the
@next/bundle-analyzerpackage to visualize your bundle sizes before a major deployment. Often, you can refactor the code to keep the heavy dependency in a Server Component and pass the result as props to a smaller Client Component. -
Infinite Loops in Server Components: Using
fetchwithcache: 'no-store'orrevalidate: 0inside a layout or page that also revalidates itself can sometimes create request loops on Vercel, especially if headers or cookies are involved. Be cautious with dynamic data fetching in shared layouts.
Final Checks Before Going Live
Before you run vercel --prod, go through this final checklist:
- Audit
fetchcalls: Does everyfetchhave an explicit caching policy (no-storeorrevalidate) that matches its purpose? - Review
'use client'boundaries: Are your Client Components as small and targeted as possible? - Check bundle size: Run the bundle analyzer to catch any unexpectedly large libraries on the client side.
- Verify environment variables: Confirm that all necessary variables are set in your Vercel project settings and that
NEXT_PUBLIC_is used correctly. - Test webhooks: If using on-demand revalidation, trigger your webhooks and confirm that the content updates as expected.
The Next.js App Router offers a more powerful and granular way to build web applications, but it demands a more disciplined approach to data fetching and state management. This checklist helps ensure your launch is smooth and predictable.