Using Next.js Server Components to Trim Client-Side JavaScript
Learn how Next.js Server Components let you fetch data and render UI on the server, reducing the JavaScript sent to the browser, with a concrete example and verification steps.
03 Jun 2026, 07:26 UTC

The problem: shipping data‑fetching logic to the browser
Many React apps still send a bare HTML shell to the browser and then rely on useEffect or data‑fetching libraries to request the actual content. This creates a flash of placeholders, extra round‑trips, and a JavaScript bundle that grows with every feature.
Next.js Server Components (RSC) let you move the data‑fetching and rendering work to the server. The browser receives the rendered markup plus a lightweight description of the UI, while the component’s JavaScript never leaves the server.
How Server Components change the data‑fetching flow
In the app/ directory (the App Router), components are Server Components by default. You can declare them as async functions and await data directly, without creating an API route just to feed the frontend.
Because the server runs the requests, you can often reach a database or file system with lower latency than a browser‑to‑API round‑trip. To avoid sequential waterfalls inside server code, start independent fetches before awaiting them (e.g., with Promise.all).
Note: Server Components cannot use React hooks like useState or useEffect, and they cannot access browser‑only globals such as window or document. Interactivity requires a Client Component marked with the 'use client' directive.
Worked example: a product detail page
The following files illustrate a Server Component that fetches product data and passes a serializable ID to a small interactive Client Component.
// app/products/[id]/page.tsx (Server Component)
// No 'use client' directive – runs only on the server.
import { ProductBuyButton } from './BuyButton';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) throw new Error('Failed to load product');
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
{product.name}
{product.description}
{/* Only this button ships as client‑side JavaScript */}
);
}
// app/products/[id]/BuyButton.tsx (Client Component)
'use client';
import { useState } from 'react';
export function ProductBuyButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
setAdded(true)}>
{added ? 'Added to Cart!' : 'Add to Cart'}
);
}
The Server Component (ProductPage) does not appear in the browser’s JavaScript bundle; only the ProductBuyButton client code is sent to the client.
Trade‑offs and limitations
- Serialization constraint. Props passed from a Server Component to a Client Component must be serializable (plain JSON‑compatible values). Functions, class instances, or non‑plain objects will cause a runtime error; convert them to plain data first.
- Latency without caching. Because the server waits for data before streaming HTML, a slow upstream source can increase Time to First Byte. Next.js caches
fetchrequests by default in many configurations, and you can control revalidation withexport const revalidate = 60or segment‑level config. Caching behavior has evolved across releases, so consult the documentation for your exact Next.js version. - Edge runtime considerations. If you deploy a route to the Edge runtime, ensure that any
fetchcalls or data sources are compatible with that environment; otherwise fall back to the Node.js runtime. - Mental‑model shift. Teams must decide, per component, whether code runs on the server or the client. Misplaced boundaries either bloat the bundle or produce build‑time errors.
Verifying the result
- Network tab. Open browser DevTools, filter by JS, reload the page. The data‑fetching logic inside
ProductPageshould not appear in any downloaded chunk. - Page source. View the raw HTML; the product name and description should be present in the initial markup, not injected after hydration.
- Hook test. Temporarily add
useStatetoProductPage. The Next.js compiler should emit an error indicating that hooks are only allowed in Client Components, confirming the file runs on the server.
If all three checks pass, you have successfully moved rendering work off the client. The next step is to audit remaining 'use client' files and push interactivity further down the tree where possible.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.