A Practical Next.js 16 Production Checklist for App Router
Shipping a Next.js 16 app? Our production checklist covers the App Router on Vercel: Server vs Client Components, caching, ISR, and common footguns we've fixed.
The App Router Shift: Why You Need a New Checklist
Next.js 13 introduced the App Router, a significant paradigm shift from the familiar Pages Router. Now stable in what we'll call Next.js 16 (the current major versioning), it fundamentally changes how we build and deploy applications. It's no longer just about file-based routing; it's about a component-centric architecture that prioritizes server-side rendering by default.
At JRV Systems, we've helped several Malaysian businesses migrate their applications or start new projects with this architecture. The learning curve is real, and the old deployment habits don't always apply. This is our internal Next.js 16 production checklist, refined from real-world project deployments on Vercel.
Server vs. Client Components: A Simple Rule
The most fundamental concept in the App Router is the distinction between Server Components (RSCs) and Client Components. The rule of thumb is simple: start with Server Components, and only opt into Client Components when necessary.
Server Components run exclusively on the server. They can directly access databases or file systems, fetch data, and they render to HTML before being sent to the browser. Crucially, they ship zero JavaScript to the client, leading to a faster initial page load.
You should switch to a Client Component by adding the 'use client' directive at the top of a file only when you need browser-specific functionality.
Use Client Components for:
- Event listeners like
onClick()oronChange() - State and lifecycle hooks, such as
useState(),useEffect(), oruseReducer() - Browser-only APIs like
localStorage,window, or the Geolocation API - Third-party libraries that depend on any of the above (e.g., many charting or animation libraries)
A common mistake is making an entire page a Client Component. Instead, keep your Client Components as small and specific as possible. For example, a product page can be a Server Component that fetches product data, while the interactive 'Add to Cart' button within it is a small, isolated Client Component.
Mastering the New Caching Defaults
One of the most impactful changes that often catches developers off guard is the new caching behavior. In the App Router, the native fetch API is extended by Next.js to automatically cache requests. This is a powerful performance feature but can lead to stale data if you're not aware of it.
By default, fetch('https://api.example.com/data') will be cached indefinitely. This is similar to the behavior of getStaticProps in the Pages Router, making your data fetches static by default.
To control this, you have a few options:
- Dynamic, No Caching:
fetch('...', { cache: 'no-store' }). This ensures the data is fetched fresh on every request, behaving likegetServerSideProps. - Time-based Revalidation (ISR):
fetch('...', { next: { revalidate: 3600 } }). This tells Next.js to cache the response but re-fetch it if a request comes in after the specified time (in this case, 3600 seconds or 1 hour).
Understanding and deliberately choosing a caching strategy for each data fetch is a critical step in any Next.js 16 production checklist. For e-commerce sites we build, product prices might be revalidated every hour, while blog post content might be cached until explicitly told to update.
On-Demand Revalidation with Tags
Time-based revalidation is useful, but what if you need to update a page the instant its content changes in your headless CMS? This is where on-demand Incremental Static Regeneration (ISR) using tags becomes essential.
The process involves two steps:
-
Tag your data fetch: When you fetch data that might need to be revalidated, you assign it a tag. For instance, fetching a list of articles could look like this:
fetch('https://my-cms/api/articles', { next: { tags: ['articles'] } }). -
Trigger revalidation via an API route: You create a secure API route (e.g.,
/api/revalidate) that can be called by your CMS using a webhook whenever an article is published or updated. Inside this route, you call therevalidateTag()function:revalidateTag('articles').
When this API route is hit, Vercel purges the cache for any data fetch associated with the articles tag. The next user who visits a page using that data will get a freshly rendered version. This provides the performance of a static site with the dynamism of a server-rendered one.
Four Common Production Footguns We've Debugged
As part of our work, we often review or debug Next.js applications for clients. The same handful of issues with the App Router appear frequently. Adding these to your checklist can save you hours of frustration.
-
Using Client-Side Hooks in Server Components: The most common error is trying to use
useStateoruseEffectin a component that doesn't have the'use client'directive. Remember, Server Components are for rendering UI, not for managing interactive state. The error messages from Next.js are quite clear, but it represents a conceptual hurdle for teams new to the model. -
Incorrect Environment Variable Exposure: The rule for environment variables remains the same, but it's a frequent point of confusion. Any variable needed in the browser (inside a Client Component) must be prefixed with
NEXT_PUBLIC_. Server Components can safely access any server-side environment variable directly viaprocess.envwithout the prefix. -
Making Entire Pages Client Components: Adding
'use client'at the top of apage.tsxfile is a performance anti-pattern. It effectively turns your entire page and all its children into a client-side rendered app, negating the primary benefit of RSCs. Always push the'use client'directive down to the most specific, interactive component that needs it. -
Data Fetching Inefficiencies: With RSCs, any component can fetch its own data. While this is great for component independence, it can lead to redundant database calls (e.g., multiple components fetching the same user profile). To mitigate this, Next.js automatically de-duplicates identical
fetchrequests. For non-fetchdata sources like a database client, you should wrap your data functions with React'scache()utility to achieve similar de-duplication within a single render pass.