The biggest mistake I see developers make with Server Components is treating them as "a faster way to render React." That misses the point entirely.
Server Components fundamentally change where your code runs and how data flows through your application. They're not an optimization — they're a new mental model.
Here's the key insight: Server Components are not components that render on the server. They are components that only exist on the server.
They never hydrate. They never re-render on the client. They send HTML and a serialized payload — not JavaScript. This means:
// app/dashboard/page.tsx — Server Component (default)
import { db } from '@/lib/database';
import { DashboardChart } from './DashboardChart';
import { UserGreeting } from './UserGreeting';
export default async function DashboardPage() {
// Direct database access. No API layer needed.
const metrics = await db.query(`
SELECT date, revenue, users
FROM daily_metrics
WHERE date > NOW() - INTERVAL '30 days'
ORDER BY date ASC
`);
const user = await db.users.findUnique({
where: { id: getCurrentUserId() },
});
return (
<div className="grid grid-cols-12 gap-6">
<UserGreeting name={user.name} />
{/* Only DashboardChart ships JavaScript to the client */}
<DashboardChart data={metrics} />
</div>
);
}
Notice what's happening: direct database access inside a component. No REST endpoint. No GraphQL resolver. No useEffect + useState + loading spinner dance. The data fetching is the component.
And UserGreeting? If it's a Server Component too, it contributes zero bytes to your client bundle. Zero. The HTML arrives fully rendered.
// app/dashboard/UserGreeting.tsx — Server Component
// This component adds 0 bytes to the client JavaScript bundle
export function UserGreeting({ name }: { name: string }) {
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
return (
<div className="col-span-12">
<h1 className="text-2xl font-bold">{greeting}, {name}</h1>
<p className="text-gray-500">Here is your dashboard overview.</p>
</div>
);
}
// app/dashboard/DashboardChart.tsx — Client Component
'use client';
import { useRef, useEffect } from 'react';
interface MetricPoint {
date: string;
revenue: number;
users: number;
}
export function DashboardChart({ data }: { data: MetricPoint[] }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
// D3, Chart.js, or custom canvas rendering
// This is the ONLY part that needs client-side JavaScript
renderChart(canvasRef.current, data);
}, [data]);
return <canvas ref={canvasRef} className="col-span-12 h-64" />;
}
The mental shift: start on the server, opt into the client. Not the other way around.