I migrated a production Next.js app to Server Components and the results surprised me:
- 80% less client-side JS
- 65% faster perceived load time
- Bundle went from 482KB to 89KB
Here's exactly how the new architecture works and where most devs mess it up.
The Fundamental Shift
Every component is a Server Component by default in Next.js 15.
No more getServerSideProps. No more getStaticProps. Just import your DB client directly and query:
// app/products/page.tsx
// This runs on the SERVER. No 'use client' needed.
async function ProductsPage() {
const products = await db.products.findMany();
return <ProductList data={products} />;
}
The Data
| Approach | JS Bundle | TTI |
|---|---|---|
| CSR (Vite) | 482KB | 1,840ms |
| Legacy SSR | 315KB | 1,120ms |
| Next.js 15 RSC | 89KB | 340ms |
89KB of JavaScript for a full page. That's the power of keeping data fetching, markdown parsing, and backend logic on the server.
The Golden Rule
Default to Server. Opt into Client only when you need it.
'use client' is needed when your component uses:
- useState / useEffect
- Browser APIs (window, document)
- Event handlers (onClick, onChange)
- DOM refs
Everything else stays on the server.
The Most Common Mistake
// Wrong: wrapping everything in 'use client'
'use client'
export default function Page() {
const data = await fetch('/api/data'); // client-side fetch!
return <DataView data={data} />;
}
This defeats the entire purpose. You just recreated CSR with extra steps.
Correct Pattern
// Server component fetches data
async function Page() {
const data = await getData(); // server-side
return <DataView data={data} />;
}
// Client component for interactivity only
'use client'
function DataView({ data }) {
const [sort, setSort] = useState('asc');
// ...interactive sorting logic
}
Server fetches. Client interacts.
Streaming is a Game-Changer
async function Page() {
return (
<>
<Suspense fallback={<HeaderSkeleton />}>
<Header /> {/* streams first */}
</Suspense>
<Suspense fallback={<SlowWidget />}>
<SlowWidget /> {/* streams when ready */}
</Suspense>
</>
);
}
Users see UI immediately. Slow parts stream in later.
5 Rules I Follow
- Server Components are the default — never add 'use client' reflexively
- Fetch data in Server Components, never in Client
- Pass server data down as props to Client Components
- Keep Client Components at the leaves of your component tree
- Use Suspense + Streaming for slow data dependencies
I wrote a complete guide with full code examples, architecture decisions, and a pitfall guide covering every migration mistake I've made.
Read the full guide on Codcompass
Free problem analysis. Full solution behind login.
Top comments (0)