React Server Components in Practice: RSC Architecture on the ToolsKu Site
What Problem Do Server Components Solve?
Pain points of traditional React apps:
Browser downloads JS bundle → React runs → API requests → UI renders
↑ slow ↑ blocking ↑ waterfall
Every page shipped the full React runtime, component code, and data-fetch logic. For content-heavy tool sites, much of that JS was unnecessary.
React Server Components (RSC) render on the server (or at build time); the browser gets HTML plus minimal Client Component JS.
Rendering Models Compared
| Model | Where it renders | JS to browser | Data fetching |
|---|---|---|---|
| CSR | Browser | All component JS | useEffect waterfalls |
| SSR | Server → HTML | All component JS (hydration) | getServerSideProps |
| RSC | Server / build | Client Component JS only | async/await in components |
| SSG | Build → HTML | Client Component JS only | Fetch at build time |
ToolsKu uses SSG + RSC: Server Components at build time; Client Components loaded on demand.
Server vs Client Component Boundary
Default: everything is a Server Component
// ✅ Server Component (default)—no 'use client'
export default async function BlogPage({ params }) {
const posts = getAllPosts(params.locale, 'blog'); // read filesystem directly
return (
<div>
<h1>{posts.length} posts</h1>
{posts.map(p => <PostCard key={p.slug} post={p} />)}
</div>
);
}
Server Components can:
- Access filesystem and databases directly
- Use async/await for data
- Import server-only modules (
fs,gray-matter, etc.)
Server Components cannot:
- Use useState, useEffect, or other Hooks
- Bind event handlers (onClick, etc.)
- Use browser APIs (window, localStorage)
When you need a Client Component
'use client'; // ← Client Component
import { useState } from 'react';
export function PdfMergeClient() {
const [files, setFiles] = useState<File[]>([]);
// Interactivity, state, browser APIs → Client Component
}
ToolsKu split:
| Type | Examples | Why |
|---|---|---|
| Server | Blog list, tutorials, nav | Static content, zero JS |
| Client | PDF merge, JSON formatter, video transcode | File API + interaction |
| Server + Client | Tool page layout (Server) + tool UI (Client) | Minimal JS |
ToolsKu RSC Architecture
Page structure
src/app/[locale]/pdf/merge/page.tsx ← Server Component
├── generateMetadata() ← SEO (build time)
├── ToolPageLayout ← Server (breadcrumbs, title)
└── PdfMergeClient.tsx ← Client ('use client')
├── File upload UI
├── pdf-lib logic
└── Download button
Code-splitting impact
Traditional CSR page JS: ~180KB (React + tool logic + UI)
RSC page JS: ~45KB (Client Components + tool logic only)
Reduction: ~75%
HTML from Server Components (title, description, breadcrumbs, FAQ) has zero JS cost.
Data fetching: build time vs runtime
// Blog: read MDX at build time
export default async function BlogPostPage({ params }) {
const { slug, locale } = await params;
const result = getRenderedPost(slug, locale, 'blog');
if (!result) notFound();
return <ContentPostArticle post={result.post} rendered={result.rendered} />;
}
// Tool page: load i18n at build time
export default async function PdfMergePage({ params }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'tools.pdf_merge' });
return (
<ToolPageLayout title={t('title')} description={t('description')}>
<PdfMergeClient />
</ToolPageLayout>
);
}
All data is fetched and rendered at next build—zero runtime data requests.
Measured Performance
ToolsKu homepage (zh-CN) resource loading:
| Metric | Pure CSR (est.) | RSC + SSG (measured) |
|---|---|---|
| First-paint JS | ~180KB | ~12KB |
| FCP | ~1.2s | ~0.4s |
| LCP | ~2.1s | ~0.8s |
| TTI | ~2.8s | ~1.2s |
| HTML size | ~5KB (shell) | ~45KB (full content) |
Larger HTML but much smaller JS—the right tradeoff for content-heavy tool sites. Users and crawlers see full content immediately.
Common Pitfalls
1. Accidentally pulling in Client boundaries
// ❌ Server Component imports Client-only dependency
import { someUtil } from './client-utils'; // if client-utils has 'use client'
// ✅ Extract shared logic to server-safe module
import { someUtil } from './shared-utils';
'use client' is module-level—importing a Client module marks the import chain as client.
2. Hooks in Server Components
// ❌ Compile error
export default function Page() {
const [state, setState] = useState(0);
}
// ✅ Split Server + Client
export default function Page() {
return <InteractivePart />; // 'use client'
}
3. Over-clientifying
// ❌ Entire page is Client
'use client';
export default function ToolPage() {
return (
<div>
<h1>PDF Merge</h1> {/* static—doesn't need client */}
<p>Merge multiple PDFs</p>
<PdfMerger /> {/* only this needs client */}
</div>
);
}
// ✅ Minimize Client boundary
export default function ToolPage() {
return (
<div>
<h1>PDF Merge</h1>
<p>Merge multiple PDFs</p>
<PdfMerger />
</div>
);
}
RSC with Static Export
ToolsKu uses output: "export". RSC renders at build time:
next build
→ iterate generateStaticParams()
→ run Server Components per (locale, path)
→ static HTML + Client Component JS chunks
→ deploy to CDN
With static export, RSC’s server-runtime benefit becomes smaller JS bundles and faster first paint.
Summary
React Server Components don’t replace Client Components—they redefine where JS is necessary. For content-first, interaction-second sites like ToolsKu, RSC renders ~90% of pages with zero JS and loads Client Components only where tools need interactivity—the key to extreme first-paint performance.
Try these browser-local tools — no sign-up required →