loading…
— Frontend / 2026
A complete enterprise-ready blueprint for organizing, caching, structuring, and scaling large Next.js (App Router) applications.

Yogesh Mishra

Modern Next.js apps fail due to sloppy boundaries, not due to React or SSR limits. This is the definitive blueprint for building something that won't collapse as features grow.
Three principles that govern every decision in a well-structured Next.js application:
Most developers treat the App Router as a black box. Understanding the actual lifecycle is what separates architects from implementors.
The critical insight: selective hydration. Only the components marked ever get JavaScript. A dashboard with 40 components might hydrate 6 of them. The other 34 ship as static HTML with zero runtime cost.
// tags Next.js · Architecture · React · AppRouter
— Reactions
If this hit, tap a reaction. It tells me what to write next.
'use client'This is where most teams get it wrong. They default to 'use client' because it feels familiar, and they pay for it in bundle size and slower Time-to-Interactive.

Draw your component tree. Colour server components green, client components red.
Green (server): layout shells, data fetching, static content, auth-gated wrappers, anything that reads from a DB or CMS.
Red (client): forms with controlled inputs, charts, animations, WebSocket listeners, anything touching window or document.
A healthy tree is 90% green. If your tree is 50/50, you're shipping a React SPA disguised as a Next.js app, and you'll pay the performance penalty every time a user hits your page on a 3G connection.
// ❌ 'use client' on a component that only reads props
'use client';
export function UserCard({ name, role }: { name: string; role: string }) {
return (
<div>
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}
// ✅ No directive needed, renders on server, ships as HTML
export function UserCard({ name, role }: { name: string; role: string }) {
return (
<div>
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}The folder structure is the architecture. Get this wrong and every feature ships with accidental complexity.
Use revalidate for semi-dynamic pages, blog posts, product pages, docs. Fresh enough for readers, fast enough for Core Web Vitals.
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1 hour
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}Tag-based invalidation for granular control, revalidate a single product without rebuilding every page.
import { revalidateTag } from 'next/cache';
// In your data layer
const product = await fetch(url, { next: { tags: [`product:${id}`] } });
// In your webhook handler
revalidateTag(`product:${id}`);Dashboard is the only route that justifies heavy client JS, charts, real-time feeds, drag-and-drop.
Not everything needs client-side hydration. Here's the whitelist:
Everything else stays server. If you're adding 'use client' to a component that just renders props, you're shipping unnecessary JavaScript.
Track what matters: TTFB, INP, hydration time, JS bundle size. Wire OpenTelemetry into your instrumentation file.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { setupOtel } = await import('./lib/otel');
setupOtel();
}
}Every PR must pass these gates before merge:
tsc --noEmitnext build must succeedimport from lib/server inside 'use client' files“Architecture isn't what you draw on a whiteboard. It's what survives contact with the twentieth feature request.