RSC Boundary Design: Server/Client Separation Criteria
How to simultaneously manage bundle size and complexity by dividing React Server Components boundaries into functional units

Introduction
By introducing RSC, data loading can be pushed to the server, improving first render performance. However, if you blindly change to a Server Component, code complexity will increase in areas where there is a lot of interaction. Especially when developing as a team, component boundaries become unstable if there is no agreement on “how far the server goes.”
This article summarizes the method of dividing RSC boundaries by function and the signs that appear when the boundary is incorrect from a practical perspective.

Problem definition
If the following situation repeats, there is a high possibility that the boundary design is incorrect.
- Excessive
"use client"is attached to each page, causing the initial JS bundle to grow rapidly. - Data fetched from the server is redundantly fetched again using a client hook.
- UI changes and data loading changes occur simultaneously, increasing the review scope.
- The cache policy is scattered on a page-by-page basis, and the revalidation strategy is inconsistent.
The key is not to put “UI interaction” and “data orchestration” on the same layer.
Key concepts
| Category | Good things to have in Server Component | Good things to put in Client Component |
|---|---|---|
| data processing | DB/API aggregate, permission-based filtering | Real-time local status, form editing status |
| Rendering | SEO Areas of Need, Initial Content | Click/drag/animate centered area |
| Cathy | revalidateTag, fetch cache | browser memory cache |
| Dependency | Node-specific SDK, private token | DOM API, Web API |
After dividing the border, the goal is to “minimize client islands.” Two to three interactive areas per page are usually sufficient.
Code example 1: Combining data across server boundaries
import { cache } from "react";
import { getPostList, getPopularTags } from "@/lib/posts/repository";
const loadPostsPage = cache(async () => {
const [posts, tags] = await Promise.all([
getPostList({ limit: 30 }),
getPopularTags({ limit: 20 }),
]);
return { posts, tags };
});
export default async function PostsPageServer() {
const { posts, tags } = await loadPostsPage();
return (
<>
<PostsHero total={posts.length} />
<PostsList posts={posts} />
<PopularTags tags={tags} />
</>
);
}
Code example 2: Separate interactions into client islands only
"use client";
import { useMemo, useState } from "react";
export function PostFilterIsland({ tags }: { tags: string[] }) {
const [query, setQuery] = useState("");
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const normalized = useMemo(() => query.trim().toLowerCase(), [query]);
return (
<section className="space-y-2">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="검색어"
className="w-full rounded-md border px-3 py-2"
/>
<TagPills tags={tags} selected={selectedTag} onSelect={setSelectedTag} />
<p className="text-sm text-muted-foreground">query={normalized || "(empty)"}</p>
</section>
);
}
Architecture flow
Tradeoffs
- If you move to the server center, the number of bundles will be reduced, but the team must understand the component division rules.
- Minimizing the client island improves performance, but UI state transfer design becomes important.
- When the boundary becomes stable, not only does page performance improve, but the review scope also becomes smaller.
Cleanup
The RSC boundary is not a matter of technical preference: “Server or client?” It is an architectural decision that separates data responsibilities from interaction responsibilities. By documenting your boundary criteria and incorporating them into your PR checklist, the overall quality of your team will steadily increase.
Image source
- Cover: source link
- License: CC BY-SA 4.0 / Author: Lora Gilyard
- Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.