A no-nonsense guide to using the Next.js App Router in production — server vs client components, data fetching, caching, streaming, SEO, and the traps that cost you Lighthouse points.
- Published
- Reading
- 4 min
- Author
- Yaroslav Tsarenko
The App Router is not a rewrite of the Pages Router. It is a different mental model. If you carry Pages Router assumptions in, you will fight the framework. If you internalize the model, you will ship faster than you did before.
This is what I actually use every day.
1. Server components are the default — keep it that way
The first thing new App Router codebases get wrong is sprinkling "use client" at the top of every file. That erases the biggest win of the router: shipping less JavaScript.
The rule I use:
- Start every component as a server component.
- Add
"use client"only at the boundary where you needuseState,useEffect, browser APIs, or event handlers. - Push that boundary as low in the tree as possible. Never at the layout level.
That last one is important. If your layout.tsx renders a client component that wraps {children}, the whole app is effectively client-rendered even though it looks server-rendered. I have seen portfolios and SaaS dashboards lose half their SEO because of a single "use client" guard in the layout.
2. Data fetching: pick one pattern per boundary
Server components can await fetch directly. This is the happy path:
export default async function Dashboard() {
const orders = await fetch("https://api.example.com/orders", {
next: { revalidate: 60 },
}).then((r) => r.json());
return <OrdersTable orders={orders} />;
}Client-side, use React Query or SWR — but only for interactive data that must live in the client. Do not double up: if the data is fetched on the server, do not re-fetch on the client "for hydration."
3. Caching is not magic — it is four separate things
Get comfortable with the four caches:
- Request memoization (per-render, per-request).
- Data Cache (persistent,
fetchresponses keyed by URL + options). - Full Route Cache (rendered HTML/RSC payload for static routes).
- Router Cache (client-side, prefetched RSC).
The one that bites everyone: mutations that change data on the server do not automatically invalidate the client Router Cache. Call revalidatePath or revalidateTag after mutations:
"use server";
export async function updateOrder(formData: FormData) {
await db.orders.update(/* ... */);
revalidateTag("orders");
}4. Streaming and loading.tsx
Streaming with <Suspense> and loading.tsx gives users a fast first paint even when the underlying data is slow. But not everything should stream — if a section is above the fold and the LCP element, block on it. Suspense for below-the-fold blocks, block above the fold.
5. Metadata API — the SEO win you will forget to use
Every route can export generateMetadata. It runs on the server, gets access to params and dynamic data, and produces <title>, OG, Twitter and canonical tags. Two things I always do:
metadataBase: new URL(...)in the root layout, so relative image URLs resolve properly.alternates: { canonical: ... }on every page. Duplicate URLs from query strings are cheap to prevent and expensive to clean up later.
6. Route handlers vs Server Actions
- Route handlers (
app/api/*/route.ts) — public HTTP endpoints, webhooks, integrations, anything that needs to be called from outside your Next app. - Server Actions — mutations from your own UI. Great DX, progressive enhancement, and they compose with
revalidateTag.
Do not build a REST API in server actions. Do not build internal form submissions in route handlers. Pick the right tool.
7. Middleware is powerful — and dangerous
Middleware runs on every request. Two lessons I learned the hard way:
- Do not put heavy logic in middleware. Cache lookups, session validation with an in-memory client — fine. Complex DB queries — not fine.
- Keep the matcher tight.
matcher: ["/dashboard/:path*"]beatsmatcher: ["/((?!_next).*)"]in both correctness and perf.
8. Deployment realities
On Vercel, App Router uses Fluid Compute by default. That means:
- 300s default execution timeout on Vercel Functions (way up from the old 60–90s).
- Middleware and route handlers now run on Node, so you get the full standard library. Edge Functions are no longer the recommended default.
- Static routes with
revalidateare cached at the edge automatically.
If you were writing Edge Functions for cold-start reasons — check the current guidance. Fluid Compute reuses instances and has made most Node functions competitive.
9. The performance checklist I actually run
- Next/Image everywhere, with real
sizes(not100vw). - Fonts via
next/fontwithdisplay: swap. - No client component in the root layout.
metadataandalternates.canonicalon every route.- Suspense boundaries only below the fold.
revalidateTagafter every mutation.
That list gets me from a blank Next.js app to a Lighthouse-90+ production build without heroics.
Closing thought
The App Router rewards understanding, not memorization. Learn the four caches, the server/client boundary, and the metadata API — everything else falls into place.
If you have a Next.js app that is slower than it should be, or an old Pages Router codebase you want to migrate cleanly — get in touch. I do exactly this kind of work as a Full-Stack Developer for teams that want it done once, properly.