DEV Community

kol kol
kol kol

Posted on • Originally published at codcompass.com

Next.js 15 Server Components — I Cut My Bundle from 482KB to 89KB

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Users see UI immediately. Slow parts stream in later.

5 Rules I Follow

  1. Server Components are the default — never add 'use client' reflexively
  2. Fetch data in Server Components, never in Client
  3. Pass server data down as props to Client Components
  4. Keep Client Components at the leaves of your component tree
  5. 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)