React Server Components ในโปรดักชัน: รูปแบบและกับดัก
React Server Components ในโปรดักชัน: รูปแบบที่ผ่านการพิสูจน์ แอนตี้แพทเทิร์นที่พบบ่อย และกลยุทธ์การดีบักสำหรับแอป Next.js 15 ที่มั่นคง

React Server Components (RSC) เปลี่ยนวิธีทำงานของการเรนเดอร์ฝั่งเซิร์ฟเวอร์ใน Next.js 15 อย่างพื้นฐาน แต่การนำไปใช้ในโปรดักชันเผยให้เห็นกับดักที่เอกสารทางการอาจไม่ได้ครอบคลุมเสมอไป บทความนี้อธิบายแยกแยะรูปแบบที่ใช้งานได้ดี รูปแบบที่พังง่าย และวิธีวินิจฉัยปัญหาก่อนที่จะไปถึงโปรดักชัน
Server Component ทำงานเฉพาะบนเซิร์ฟเวอร์และส่ง JavaScript ไปยังเบราว์เซอร์เป็นศูนย์ ส่วน Client Component (ทำเครื่องหมายด้วย "use client") ทำงานทั้งสองฝั่ง กฎคือ ให้ Client Components มีขนาดเล็กที่สุดและอยู่ตำแหน่งต่ำที่สุดในต้นไม้คอมโพเนนต์
ขอบเขตเซิร์ฟเวอร์-ไคลเอนต์: เข้าใจรูปแบบ boundary
กับดัก RSC ที่พบบ่อยที่สุดคือเรื่องขอบเขตระหว่าง Server และ Client Components เมื่อคอมโพเนนต์ใดมีไดเรกทีฟ "use client" แล้ว ลูกที่ถูก import เข้ามาทั้งหมดจะกลายเป็น Client Components ตามไปด้วย แม้จะไม่มีไดเรกทีฟก็ตาม
import { ProductDetails } from './ProductDetails'
import { AddToCartButton } from './AddToCartButton'
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getProduct(id)
return (
<div>
{/* Server Component: เข้าถึง DB โดยตรง */}
<ProductDetails product={product} />
{/* Client Component: ความสามารถโต้ตอบที่แยกออกมา */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
)
}'use client'
import { useState } from 'react'
export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
const [adding, setAdding] = useState(false)
async function handleAdd() {
setAdding(true)
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId, quantity: 1 }),
})
setAdding(false)
}
return (
<button onClick={handleAdd} disabled={adding}>
{adding ? 'กำลังเพิ่ม...' : `เพิ่มในตะกร้า — $${price}`}
</button>
)
}รูปแบบสำคัญ: ส่งข้อมูลในรูป props ที่ serialize ได้ จาก Server Component ไปยัง Client Component ฟังก์ชัน คลาส และอ็อบเจกต์ Date ไม่สามารถข้ามขอบเขตนี้ได้
แอนตี้แพทเทิร์น: Wrapper แบบ Client Component ที่ไม่จำเป็น
ความผิดพลาดที่พบบ่อยคือสร้าง Client Component ที่ห่อหุ้มลูกที่เป็น Server Component ทำให้ทั้งสาขาย่อยถูกบังคับให้กลายเป็นฝั่งไคลเอนต์
'use client'
import { useState } from 'react'
// เนื้อหาลูกทั้งหมดถูกย้ายไปฝั่งไคลเอนต์
export function PageWrapper({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light')
return (
<div className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
สลับธีม
</button>
{children}
</div>
)
}วิธีแก้: ส่ง Server Components ผ่าน children (รูปแบบ slot) ลูกที่ถูกส่งเป็น props จะยังเป็น Server Components แม้ว่า parent จะเป็น Client Component โค้ดข้างต้นทำงานได้อย่างถูกต้องตราบใดที่ children มาจาก parent ที่เป็น Server Component
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'
export default function Layout() {
return (
<PageWrapper>
{/* ยังคงเป็น Server Component แม้มี wrapper ที่เป็นไคลเอนต์ */}
<HeavyServerContent />
</PageWrapper>
)
}รูปแบบ composition นี้รักษาประโยชน์ของการเรนเดอร์ฝั่งเซิร์ฟเวอร์สำหรับเนื้อหาหนัก ในขณะที่เปิดให้มีปฏิสัมพันธ์ในระดับ wrapper
การจัดการข้อมูลแบบ async: รูปแบบ fetch ในคอมโพเนนต์
React 19 และ Next.js 15 รองรับ async/await ใน Server Components โดยตรง รูปแบบนี้ทำให้การดึงข้อมูลง่ายขึ้นเมื่อเทียบกับวิธี getServerSideProps แบบเดิม
import { cache } from 'react'
// ลดการเรียกซ้ำที่เหมือนกันภายในการเรนเดอร์เดียวกัน
const getUser = cache(async (userId: string) => {
const res = await fetch(`https://api.example.com/users/${userId}`, {
next: { revalidate: 3600 }, // แคชเป็นเวลา 1 ชั่วโมง
})
if (!res.ok) throw new Error('User not found')
return res.json()
})
export default async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId)
return (
<section>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>เป็นสมาชิกตั้งแต่ {new Date(user.createdAt).toLocaleDateString('th-TH')}</p>
</section>
)
}สามจุดสำคัญ:
cache()ของ React ลดการเรียกซ้ำที่เหมือนกันในการเรนเดอร์เซิร์ฟเวอร์เดียวnext: { revalidate }ควบคุมระยะเวลาแคชฝั่ง Next.js- ข้อผิดพลาดในรูปแบบ Server Component แบบ async จะกระตุ้น
error.tsxที่อยู่ใกล้ที่สุด
กับดัก serialize: สิ่งที่ข้ามขอบเขตไม่ได้
ข้อมูลที่แลกเปลี่ยนระหว่าง Server และ Client Components ต้อง serialize เป็น JSON ได้ ต่อไปนี้คือสิ่งที่ทำให้เกิดข้อผิดพลาดเงียบหรือ crash
// กับดัก: ส่งประเภทที่ serialize ไม่ได้
// ฟังก์ชัน — ใช้ไม่ได้
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// ใช้ Server Action ที่ import เข้ามาแทน
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />
// อ็อบเจกต์ Date — ใช้ไม่ได้
<ClientComp createdAt={new Date()} />
// สตริง ISO — ใช้ได้
<ClientComp createdAt={new Date().toISOString()} />
// Map, Set, RegExp — ใช้ไม่ได้
<ClientComp data={new Map([['key', 'value']])} />
// อ็อบเจกต์ธรรมดาหรืออาเรย์ — ใช้ได้
<ClientComp data={{ key: 'value' }} />Server Actions (ฟังก์ชันที่มี "use server") คือข้อยกเว้น สามารถส่งเป็น props ไปยัง Client Component ได้ เพราะ Next.js แปลงให้เป็น HTTP endpoint
พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Streaming และ Suspense: รูปแบบโหลดแบบค่อยเป็นค่อยไป
SSR streaming ร่วมกับ Suspense ส่ง HTML ไปยังเบราว์เซอร์แบบทยอยส่ง รูปแบบที่ดีที่สุดใช้ Suspense boundary ละเอียดล้อมรอบทุกส่วนแบบ async
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { UserStats } from './UserStats'
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}แต่ละส่วนโหลดเป็นอิสระ ถ้า RevenueChart ใช้เวลา 3 วินาที และ UserStats ใช้ 200 มิลลิวินาที ส่วนสถิติจะปรากฏทันทีโดยไม่ต้องรอกราฟ
เนื้อหาภายใน Suspense boundary ถูกเรนเดอร์ฝั่งเซิร์ฟเวอร์และอยู่ใน HTML ตั้งต้น Crawler มองเห็นเนื้อหาทั้งหมด Streaming ส่งผลเฉพาะกับความเร็วในการส่งไปยังเบราว์เซอร์ ไม่ส่งผลต่อการมองเห็น SEO
ดีบักในโปรดักชัน: ตามรอยปัญหา RSC
ข้อผิดพลาด RSC มักดูเข้าใจยาก สามเทคนิควินิจฉัยที่ใช้ได้จริงในโปรดักชัน
1. ระบุ hydration mismatch
'use client'
import { useEffect, useState } from 'react'
export function HydrationDebug() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (process.env.NODE_ENV !== 'development') return null
return (
<div style={{ position: 'fixed', bottom: 0, right: 0, padding: '4px 8px', fontSize: 12 }}>
{isClient ? 'Client' : 'Server'}
</div>
)
}2. บันทึกล็อก payload ของ RSC
ใน Next.js 15 เปิดใช้การล็อก RSC ใน next.config.ts:
const nextConfig = {
logging: {
fetches: {
fullUrl: true, // แสดง URL fetch แบบเต็ม
},
},
}
export default nextConfig3. ตรวจสอบขนาด payload
payload RSC ขนาดเกิน (> 128 KB) ทำให้ประสิทธิภาพลดลง ควรเฝ้าดูคำขอเครือข่ายที่มี content type text/x-component ใน DevTools
รูปแบบขั้นสูง: composition กับ Server Actions
Server Actions ผสมกับ Server Components สร้างรูปแบบ CQRS ตามธรรมชาติ: อ่านข้อมูลที่เซิร์ฟเวอร์ (RSC) เขียนข้อมูลผ่าน actions
import { getTodos } from '@/lib/services/todo'
import { TodoForm } from './TodoForm'
import { deleteTodo } from '@/lib/actions/todo'
export default async function TodoList() {
const todos = await getTodos()
return (
<div>
<TodoForm />
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.title}
<form action={deleteTodo}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">ลบ</button>
</form>
</li>
))}
</ul>
</div>
)
}'use server'
import { revalidatePath } from 'next/cache'
import { TodoService } from '@/lib/services/todo'
export async function deleteTodo(formData: FormData) {
const id = formData.get('id') as string
await TodoService.delete(id)
revalidatePath('/todos')
}การเรียก revalidatePath กระตุ้นการเรนเดอร์ Server Component ใหม่ด้วยข้อมูลที่อัปเดต โดยไม่ต้องโหลดหน้าใหม่ทั้งหมด
หากต้องการเตรียมสัมภาษณ์ในหัวข้อเหล่านี้อย่างลึกซึ้งยิ่งขึ้น ขอแนะนำให้ดูโมดูล Next.js Server Actions และโมดูล Next.js Data Fetching บน SharpSkill เอกสารทางการของ React ครอบคลุมข้อกำหนด Server Components ทั้งหมด
สรุป
- ให้ Client Components มีขนาดเล็กและแยกอยู่ที่ฐานล่างของต้นไม้คอมโพเนนต์
- ใช้รูปแบบ slot (
children) เพื่อรักษา Server Components ภายใน wrapper ที่เป็นไคลเอนต์ - ตรวจสอบเสมอว่า props ที่ข้ามขอบเขตเซิร์ฟเวอร์-ไคลเอนต์สามารถ serialize ได้
- วาง Suspense boundary ละเอียดรอบทุกส่วน async ที่เป็นอิสระ
- ตรวจสอบขนาด payload RSC ในโปรดักชัน (เป้าหมาย < 128 KB)
- รวม Server Components (อ่าน) และ Server Actions (เขียน) เพื่อรูปแบบ CQRS ตามธรรมชาติ
- ใช้
cache()ของ React เพื่อลดการเรียกซ้ำของคำขอภายในการเรนเดอร์เซิร์ฟเวอร์เดียว
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

React 19: Server Components ในระบบ Production - คู่มือฉบับสมบูรณ์
เชี่ยวชาญ React 19 Server Components ในระบบ Production สถาปัตยกรรม รูปแบบการออกแบบ Streaming Caching และการเพิ่มประสิทธิภาพสำหรับแอปพลิเคชันที่มีประสิทธิภาพสูง

React Compiler ในปี 2026: Automatic Memoization และคำถามสัมภาษณ์งาน
เรียนรู้ React Compiler ที่ทำ memoization อัตโนมัติ พร้อมคำถามสัมภาษณ์งานยอดนิยมสำหรับนักพัฒนา React ในปี 2026

React Hooks ขั้นสูง: รูปแบบและการเพิ่มประสิทธิภาพ
เชี่ยวชาญ React Hooks ขั้นสูงด้วยรูปแบบที่พิสูจน์แล้ว Custom hooks, useEffect ที่ปรับแต่ง, useMemo, useCallback และเทคนิคด้านประสิทธิภาพ