React Server Components in Practice: RSC Architecture on the ToolsKu Site

技术架构(Updated May 21, 2026)

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 →

#React#Server Components#Next.js#SSR#性能