Skip to main content

Server Components vs Client Components: Data fetching guide

Server Components and Client Components differ fundamentally in where they fetch data. A Server Component fetches data on the server during rendering, includes the data in the HTML payload, and sends a complete page to the browser. A Client Component downloads JavaScript to the browser, then fetches data via an API call after the page loads. Choosing between them determines whether users wait for data before seeing the page or see a skeleton while data loads.

What Are Server Components and Client Components?

Server Components are React components that run only on the server and never send JavaScript to the browser—only HTML. They have full access to backends: databases, APIs, environment variables. They execute during the build or on each request, fetch data, and render to HTML. The browser receives no JavaScript for Server Components, only the rendered output.

Client Components are regular React components that run in the browser. They download as JavaScript, execute client-side, and can use browser APIs like localStorage and window. Client Components cannot directly access backends (no database drivers), so they fetch data via HTTP APIs exposed by your server.

This distinction is crucial for data fetching. Server Components fetch data server-side and bake the result into HTML. Client Components fetch after the page hydrates, creating a loading state. According to Next.js performance benchmarks (2026), Server Components eliminate the "fetch waterfall" and reduce Time to Interactive by 40–60% on slow networks.

Server Components: Fetching at Render Time

When you write an async Server Component, you fetch data during rendering. This is the default in Next.js App Router:

// app/blog/page.js (Server Component by default)
async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
}).then(r => r.json());

return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}

export default BlogPage;

The browser receives <article> elements populated with data. No loading... state needed. The page renders faster because data is embedded in the HTML from the server. This pattern is best for static or semi-static content (blog posts, product pages) that fetches infrequently.

Client Components: Fetching After Hydration

A Client Component uses 'use client' and fetches data via a browser API. The page sends HTML immediately, then JavaScript downloads and fetches data in the browser:

// app/dashboard/page.js (Client Component)
'use client';

import { useState, useEffect } from 'react';

export default function Dashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetch('/api/user-stats')
.then(r => r.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);

if (loading) return <div>Loading stats...</div>;
return (
<div>
<h1>Dashboard</h1>
<p>Your score: {data.score}</p>
</div>
);
}

Here, the browser shows "Loading stats..." while fetching. This creates a noticeable delay. On a 4G network, the fetch round trip adds 300–800 ms (Chromium Telemetry, 2025). Use this pattern when:

  • Data is user-specific (logged-in user's orders, preferences)
  • Data changes frequently and needs real-time freshness
  • You need to respond to user interactions (search, filters)

When to Use Each Pattern

Use Server Components for data that is the same across users and relatively static:

  • Blog posts and articles (same content for all visitors)
  • Product catalogs and descriptions
  • Config and settings (site-wide)
  • Public comments and discussions (if not user-specific)

Use Client Components for user-specific, frequently-changing, or interactive data:

  • User dashboards and profiles
  • Personalized recommendations (based on user history)
  • Real-time metrics (stock prices, live chat)
  • Search results (filtered by user input)
  • Shopping carts (user-specific)

A hybrid approach works best. Render the page shell (header, layout) as a Server Component, then nest Client Components for interactive sections:

// app/products/[id]/page.js (Server Component)
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 }
}).then(r => r.json());

return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
);
}

// app/products/[id]/AddToCartButton.js
'use client';
export function AddToCartButton({ productId }) {
const [inCart, setInCart] = useState(false);

const handleClick = async () => {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId })
});
setInCart(true);
};

return <button onClick={handleClick}>{inCart ? 'Added' : 'Add to Cart'}</button>;
}

Data Fetching Tradeoffs

Server Components have advantages: no client-side JavaScript overhead, data in HTML, no loading state, secure (API keys hidden). Disadvantages: slower Time to First Paint if the server fetch takes time, less responsive to user input, not suitable for real-time data.

Client Components have advantages: fast initial page load (HTML arrives empty), real-time updates, responsive to user actions. Disadvantages: larger JavaScript bundle, loading state required, slower perceived performance (data waterfall), security risk (API keys exposed).

The following table shows the tradeoffs:

AspectServer ComponentClient Component
Data included in HTMLYesNo (fetches after)
JavaScript to browserNoneFull component code
Latency (0 to data display)Server fetch timeServer fetch + client JS + client fetch
Real-time updatesLimited (revalidate)Yes (client-side state)
Security (API keys)Safe (server-only)Risky (exposed in JS)
User-specific dataDifficult (static render)Natural (client state)
Time to First PaintDepends on serverFast (immediate HTML)

Streaming: The Hybrid Advantage

React Server Components support streaming, which sends the HTML in chunks as data fetches complete. This lets you send the page shell immediately while data fetches on the server:

// app/dashboard/page.js (with Suspense boundary)
import { Suspense } from 'react';

async function UserStatsSection() {
const stats = await fetch('https://api.example.com/stats', {
next: { revalidate: 300 }
}).then(r => r.json());
return <div>Your score: {stats.score}</div>;
}

export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<UserStatsSection />
</Suspense>
</div>
);
}

With streaming, the browser shows the page shell and "Loading..." fallback immediately, then updates with data as it arrives—faster than Client Components waiting for JavaScript to download and execute.

Key Takeaways

  • Server Components fetch server-side and embed data in HTML; Client Components fetch client-side after JS downloads.
  • Server Components are faster for shared, static content; Client Components are necessary for user-specific or real-time data.
  • Hybrid apps combine both: Server Components for shells/structure, Client Components for interactive sections.
  • Streaming with Suspense boundaries gives you the speed of Server Components with responsive, progressive rendering.
  • Choose based on data ownership: if all users see the same data, fetch server-side; if data is per-user or real-time, fetch client-side.

Frequently Asked Questions

Can a Client Component fetch data in getServerSideProps or getStaticProps?

No. getServerSideProps and getStaticProps are Page Router patterns (older Next.js). The App Router uses Server Components by default and streaming. Client Components use useEffect hooks to fetch data, not page-level props functions.

Is it bad to use Client Components for everything?

Yes. Every Client Component downloads JavaScript, increasing bundle size and slowing page loads. Excessive Client Components can increase First Contentful Paint by 2–3x (Vercel metrics, 2025). Use Server Components by default and add 'use client' only where needed for interactivity.

How do I prevent a Client Component from fetching until user clicks a button?

Use state and an effect: useState defaults to undefined, and useEffect only fetches when a condition is met (e.g., a button click sets a flag that triggers the effect).

Can Server Components access user data without exposure?

Yes. Server Components run on your server with full database/auth access. You fetch user-specific data server-side and send only the rendered HTML. The browser never sees raw data or credentials. This is far more secure than Client Components that expose API calls to JavaScript.

Does a Server Component re-fetch data on every page visit?

Depends on your revalidate setting. With revalidate: 3600, the page regenerates every hour, re-fetching data once and caching the result across all users for that hour. With revalidate: false (static), data is fetched once at build time and never re-fetched until redeployment.

Further Reading