For a long time, the React mental model was simple: everything is a client-side component that hydrates on the browser. But as our applications grew, so did our bundles. I’ve spent the last year migrating several production projects to the App Router, and the biggest takeaway is that mastering react server components best practices isn’t just about moving useEffect calls to the server—it’s about a fundamental shift in how we think about the network boundary.
The Challenge: The Hydration Tax
The core problem we’ve all faced is the ‘Hydration Tax.’ When we use traditional SSR, the server sends HTML, but the browser still has to download all the JavaScript for those components to make them interactive. This leads to a high Total Blocking Time (TBT) and a sluggish user experience, especially on mobile devices.
In my experience, the biggest mistake developers make is treating Server Components as just ‘another way to fetch data.’ If you simply wrap your existing client components in a server-side wrapper, you’re not solving the bundle size problem; you’re just moving the fetch call.
Solution Overview: The Component Split
The solution is a strict separation of concerns. React Server Components (RSCs) allow us to keep the heavy lifting—database queries, filesystem access, and large dependencies—on the server. Only the lean, interactive elements are sent to the client as ‘Client Components’.
To get this right, I follow a simple rule of thumb: Push interactivity to the leaves. Keep your layout and data-fetching logic in Server Components, and only use 'use client' for the smallest possible pieces of the UI that actually require state or browser APIs.
Techniques for High-Performance RSCs
1. Colocating Data Fetching
One of the best react server components best practices is to fetch data exactly where it is used. Forget the days of ‘lifting state’ or passing props through five levels of components. In RSCs, you can make your component async and await your data directly.
// This is a Server Component by default
async function UserProfile({ userId }: { userId: string }) {
// Direct database call - no API route needed!
const user = await db.user.findUnique({ where: { id: userId } });
return (
<div>
<h1>{user.name}</h1>
<UserSettings user={user} /> {/* Client Component at the leaf => Best Practice!}</div>
</div>
);
}
2. Handling Client Components as Slots
A common pitfall is trying to import a Server Component into a Client Component. This is impossible because Server Components cannot be executed on the client. To solve this, pass the Server Component as children or a prop to the Client Component.
As shown in the architecture diagram above, this preserves the server-side rendering of the child while allowing the parent to handle client-side state.
3. Optimizing the Waterfall
While async components are powerful, they can create ‘waterfalls’ if you await them sequentially. I recommend using Promise.all() for independent fetches or utilizing the Suspense boundary to stream content as it arrives.
async function Page() {
// Start both fetches in parallel
const userDataPromise = getUser();
const postsDataPromise = getPosts();
return (
<div>
<Suspense fallback=<Skeleton />
<UserComponent promise={userDataPromise} />
</Suspense>
<Suspense fallback=<Skeleton />
<PostsComponent promise={postsDataPromise} />
</Suspense>
</div>
);
}
Implementation: A Real-World Case Study
I recently refactored a dashboard that had a 450KB initial JS bundle. By implementing these react server components best practices, we achieved the following:
- Bundle Reduction: We moved the heavy
date-fnsandlucide-reactlogic into Server Components, dropping the client bundle by 120KB. - TTI Improvement: Time to Interactive dropped from 3.2s to 1.4s on average 4G connections.
- Simplified Logic: We eliminated three redundant
useEffecthooks and twouseStatecalls used solely for initial data loading.
If you are struggling with performance, I highly recommend optimizing your Lighthouse score in Next.js to identify exactly which components are bloating your bundle.
Pitfalls to Avoid
- Overusing ‘use client’: Don’t put
'use client'at the top of your layout file. This turns your entire application into a client-side app, defeating the purpose of RSCs. - Passing Non-Serializable Props: You cannot pass functions or Class instances from Server to Client components. Stick to JSON-serializable data (strings, numbers, plain objects).
- Ignoring Caching: RSCs are cached by default in Next.js. If your data changes frequently, ensure you use
revalidatePathorrevalidateTagto keep the UI fresh.
When choosing your framework for this architecture, you might wonder about Next.js vs Remix in 2026; while both handle server-side logic, Next.js currently provides the most mature implementation of the RSC specification.
Final Thoughts
Adopting these react server components best practices requires a mental shift from “everything is a component” to “where does this component live?” By treating the server as a first-class citizen in your component tree, you create apps that are faster, more secure, and easier to maintain.