Next.js Client vs Server Component คืออะไร ต่างกันยังไง

การพัฒนาเว็บไซต์ด้วย Next.js ในปัจจุบันได้เปลี่ยนแปลงไปอย่างมากจากสิ่งที่เราคุ้นเคย โดยเฉพาะอย่างยิ่งเมื่อ Next.js เวอร์ชัน 13 เปิดตัว App Router มาใช้งาน สิ่งที่ตามมาคือแนวคิดใหม่ในการแบ่ง Component ระหว่าง Server และ Client ซึ่งหลายคนอาจสับสนว่ามันคืออะไร และเราควรเลือกใช้แบบไหนถึงจะเหมาะสม บทความนี้จะพาทุกคนมาไขข้อสงสัยเหล่านี้กันอย่างละเอียด โดยเริ่มตั้งแต่พื้นฐานจนไปถึงการนำไปใช้จริง เพื่อให้ทุกคนเข้าใจหลักการทำงานและสามารถตัดสินใจได้อย่างถูกต้อง

สำหรับนักพัฒนาที่เพิ่งเริ่มต้นกับ Next.js การเข้าใจความแตกต่างระหว่าง Server Component และ Client Component ถือเป็นสิ่งสำคัญอันดับต้นๆ ที่ต้องเรียนรู้ เพราะมันส่งผลโดยตรงต่อประสิทธิภาพของเว็บไซต์ ความเร็วในการโหลด และประสบการณ์ของผู้ใช้งาน ในขณะเดียวกัน การเลือกใช้ผิดชนิดอาจทำให้แอปพลิเคชันทำงานช้าลง หรือบางกรณีอาจทำให้ฟีเจอร์บางอย่างไม่ทำงานเลยก็เป็นได้ ดังนั้นการเข้าใจหลักการพื้นฐานจึงเป็นรากฐานที่จำเป็นสำหรับทุกคนที่ต้องการใช้ Next.js อย่างมีประสิทธิภาพ

ยิ่งไปกว่านั้น ในยุคที่ SEO และ Core Web Vitals มีความสำคัญมากขึ้นเรื่อยๆ การเลือกใช้ Component อย่างเหมาะสมจะช่วยให้เว็บไซต์ของเราติดอันดับการค้นหาที่ดีขึ้น รวมถึงมีคะแนน Performance ที่สูงขึ้นด้วย ซึ่งทั้งหมดนี้จะเป็นประโยชน์อย่างยิ่งสำหรับธุรกิจที่ต้องการดึงดูดลูกค้าผ่านทางอินเทอร์เน็ต ดังนั้นเรามาเริ่มต้นทำความเข้าใจกันตั้งแต่ต้นกันเลยดีกว่า

Modern tech illustration showing a developer workspace with split screen concept, left side blue ser

Next.js App Router คืออะไร ทำไมต้องรู้

App Router คือระบบการจัดการเส้นทาง (Routing) และโครงสร้างโปรเจกต์แบบใหม่ที่ Next.js เพิ่มเข้ามาในเวอร์ชัน 13 โดยตั้งแต่เวอร์ชันก่อนหน้านี้ Next.js ได้ใช้ระบบที่เรียกว่า Pages Router ซึ่งเป็นวิธีดั้งเดิมในการสร้างเว็บเพจโดยการสร้างไฟล์ในโฟลเดอร์ pages แต่เมื่อ Next.js พัฒนาขึ้นเรื่อยๆ ทีมพัฒนาจึงตัดสินใจสร้างระบบใหม่ที่มีความสามารถมากกว่าเดิม ซึ่งก็คือ App Router นั่นเอง

Pages Router และ App Router มีความแตกต่างกันอย่างชัดเจนในหลายด้าน สำหรับ Pages Router เราจะใช้โฟลเดอร์ pages เป็นหลักในการจัดเก็บไฟล์ และแต่ละไฟล์จะกลายเป็นเส้นทาง (Route) อัตโนมัติ ตัวอย่างเช่น ไฟล์ pages/about.js จะเป็นเส้นทาง /about ในขณะที่ App Router ใช้โฟลเดอร์ app แทน โดยเน้นการจัดการเส้นทางผ่านโฟลเดอร์และไฟล์พิเศษที่มีชื่อขึ้นต้นด้วยเครื่องหมายวงเล็บ (Parentheses) เช่น (marketing) หรือ (shop) ซึ่งช่วยให้สามารถจัดกลุ่มเส้นทางได้อย่างมีประสิทธิภาพมากขึ้น

ยิ่งไปกว่านั้น App Router ยังนำแนวคิด Server Components มาเป็นค่าเริ่มต้น (Default) ทำให้ทุก Component ใน App Router จะถูก Render บน Server โดยอัตโนมัติ เว้นแต่เราจะกำหนดให้เป็น Client Component อย่างชัดเจน ซึ่งต่างจาก Pages Router ที่ทุกอย่างจะถูก Render บน Client เป็นหลัก การเปลี่ยนแปลงนี้ส่งผลให้ App Router มีประสิทธิภาพด้านการโหลดข้อมูลและ SEO ที่ดีกว่า เนื่องจาก Server สามารถดึงข้อมูลและส่ง HTML ที่สมบูรณ์กลับไปยังผู้ใช้ได้โดยไม่ต้องรอให้ JavaScript โหลดเสร็จก่อน

Server Component คืออะไร

Server Component คือ Component ที่ทำงานบน Server โดยเฉพาะ ซึ่งหมายความว่าโค้ดทั้งหมดจะถูกประมวลผลบนฝั่ง Server ก่อนที่จะส่งผลลัพธ์เป็น HTML ไปยังเบราว์เซอร์ของผู้ใช้ ใน Next.js เวอร์ชันปัจจุบัน Server Component ถือเป็นค่าเริ่มต้น (Default) ของทุก Component ที่สร้างใน App Router ดังนั้นเมื่อใดก็ตามที่เราสร้างไฟล์ใหม่ในโฟลเดอร์ app มันจะถูกถือว่าเป็น Server Component โดยอัตโนมัติโดยไม่ต้องกำหนดค่าอะไรเพิ่มเติม

ประโยชน์หลักของ Server Component คือการลดขนาดของ JavaScript ที่ต้องส่งไปยัง Client ทำให้เว็บไซต์โหลดเร็วขึ้นอย่างมาก ยิ่งไปกว่านั้น Server Component ยังสามารถเข้าถึงทรัพยากรต่างๆ บน Server ได้โดยตรง เช่น ฐานข้อมูล ระบบไฟล์ หรือ API ภายใน โดยไม่ต้องส่ง Request ผ่าน API Routes ซึ่งช่วยลดความซับซ้อนในการเขียนโค้ดลงได้มาก นอกจากนี้ Server Component ยังมีความปลอดภัยสูงกว่า เพราะโค้ดที่เกี่ยวข้องกับความลับ เช่น API Keys หรือ Database Connection Strings จะไม่ถูกส่งไปยัง Client เลย

ตัวอย่าง Server Component ในชีวิตจริง

เพื่อให้เข้าใจ Server Component มากขึ้น เรามาดูตัวอย่างการใช้งานจริงกัน ในการสร้างหน้าแสดงรายการบทความ เราสามารถสร้าง Server Component ที่ดึงข้อมูลจากฐานข้อมูลโดยตรงได้ โดยไม่ต้องผ่าน API ใดๆ ทั้งสิ้น ตัวอย่างด้านล่างจะแสดงให้เห็นว่าโค้ดมีลักษณะอย่างไร


// app/blog/page.tsx
import { db } from '@/lib/database';

async function getPosts() {
  // ดึงข้อมูลจากฐานข้อมูลโดยตรง
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });
  return posts;
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <main className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">บทความล่าสุด</h1>
      <div className="space-y-4">
        {posts.map((post) => (
          <article key={post.id} className="border rounded-lg p-4">
            <h2 className="text-xl font-semibold">{post.title}</h2>
            <p className="text-gray-600 mt-2">{post.excerpt}</p>
            <time className="text-sm text-gray-500 mt-2 block">
              {new Date(post.createdAt).toLocaleDateString('th-TH')}
            </time>
          </article>
        ))}
      </div>
    </main>
  );
}

จากตัวอย่างข้างต้น เราจะเห็นว่าฟังก์ชัน getPosts() สามารถเรียกใช้ฐานข้อมูลโดยตรงได้เลย ซึ่งเป็นสิ่งที่ทำไม่ได้ใน Client Component ทั่วไป ข้อดีของวิธีนี้คือเราสามารถเขียนโค้ดได้ง่ายขึ้น ประสิทธิภาพดีขึ้น และไม่ต้องกังวลเรื่องการจัดการ Loading States ที่ซับซ้อน เพราะข้อมูลจะถูกดึงมาตั้งแต่ตอนที่ Server ประมวลผลหน้าเพจ ทำให้ผู้ใช้ได้รับเนื้อหาที่สมบูรณ์ทันทีที่เปิดหน้าเว็บ

Clean infographic showing server-side rendering flow in Next.js, isometric diagram of user laptop se

Client Component คืออะไร

Client Component คือ Component ที่ทำงานบนฝั่ง Client หรือก็คือเบราว์เซอร์ของผู้ใช้นั่นเอง ในการกำหนดให้ Component เป็น Client Component เราต้องเพิ่มคำสั่ง “use client” ที่ด้านบนสุดของไฟล์อย่างชัดเจน ซึ่งต่างจาก Server Component ที่เป็นค่าเริ่มต้นโดยไม่ต้องประกาศอะไร Client Component จำเป็นต้องทำเช่นนี้เพราะ Next.js ต้องการทราบล่วงหน้าว่า Component ไหนจะต้องถูกส่ง JavaScript Bundle ไปยัง Client เพื่อให้เบราว์เซอร์สามารถประมวลผลได้อย่างถูกต้อง

เหตุผลหลักที่เราต้องใช้ Client Component คือเมื่อเราต้องการใช้งานฟีเจอร์ที่ทำงานบน Client โดยเฉพาะ เช่น React Hooks ทั้งหมด รวมถึง useState useEffect useContext และอื่นๆ ซึ่งเป็นฟังก์ชันที่ต้องทำงานบนเบราว์เซอร์เท่านั้น นอกจากนี้ Client Component ยังจำเป็นเมื่อเราต้องการใช้ Event Handlers เช่น onClick onChange onSubmit หรือต้องการจัดการ State ภายใน Component อีกด้วย โดยทั่วไปแล้ว ส่วนประกอบของเว็บที่มีการโต้ตอบกับผู้ใช้ เช่น ปุ่มกด แบบฟอร์ม หรือเมนูแบบ Dropdown มักจะต้องเป็น Client Component

ตัวอย่าง Client Component

การใช้งาน Client Component พบได้บ่อยมากในการพัฒนาเว็บไซต์จริง โดยเฉพาะกับระบบ Like Button ที่ต้องอัปเดตจำนวน Like ทันทีเมื่อผู้ใช้คลิก หรือระบบ Counter ที่ต้องนับเลขขึ้นลงตามการกดปุ่ม ตัวอย่างด้านล่างจะแสดงให้เห็นการใช้งาน useState และ Event Handler ใน Client Component อย่างชัดเจน


'use client';

import { useState } from 'react';

interface LikeButtonProps {
  initialLikes: number;
  postId: string;
}

export default function LikeButton({ initialLikes, postId }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLoading, setIsLoading] = useState(false);

  const handleLike = async () => {
    if (isLoading) return;
    
    setIsLoading(true);
    setLikes((prev) => prev + 1);
    
    try {
      await fetch(`/api/posts/${postId}/like`, {
        method: 'POST',
      });
    } catch (error) {
      // ถ้าเกิดข้อผิดพลาด ให้ลบ Like ที่เพิ่มไปออก
      setLikes((prev) => prev - 1);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button
      onClick={handleLike}
      disabled={isLoading}
      className="flex items-center gap-2 px-4 py-2 bg-pink-500 text-white rounded-full hover:bg-pink-600 transition-colors disabled:opacity-50"
    >
      <span>❤️</span>
      <span>{likes.toLocaleString()}</span>
    </button>
  );
}

จากตัวอย่างข้างต้น จะเห็นว่าเราต้องใช้คำสั่ง “use client” ที่ด้านบนสุดเสมอเมื่อต้องการใช้งาน React Hooks และ Event Handlers โดย Component นี้รับค่า initialLikes จาก Server Component ที่เป็น Parent แล้วจัดการ State ภายในตัวเอง ซึ่งเป็นรูปแบบการทำงานร่วมกันระหว่าง Server และ Client Component ที่พบได้บ่อยมากในการพัฒนาแอปพลิเคชันจริง

สรุปความต่างแบบเข้าใจง่าย

หลังจากที่เราได้เรียนรู้พื้นฐานของ Server Component และ Client Component กันแล้ว ตอนนี้เรามาสรุปความแตกต่างทั้งหมดให้เห็นภาพชัดเจนยิ่งขึ้น การเข้าใจตารางเปรียบเทียบด้านล่างจะช่วยให้ทุกคนสามารถตัดสินใจได้อย่างถูกต้องว่าควรเลือกใช้ Component ประเภทไหนในสถานการณ์ต่างๆ ซึ่งจะเป็นประโยชน์อย่างมากในการพัฒนาแอปพลิเคชันที่มีประสิทธิภาพสูง

ผู้ใช้ต้องการให้ฉันเขียนบทความ tech blog ภาษาไทยเกี่ยวกับ Next.js โดยเฉพาะเรื่อง Server Components vs Client Components ต่อจากหัวข้อที่ให้มา

  1. การผสมผสานทั้งสองแบบในโปรเจกต์จริง
Modern tech illustration showing a Next.js app architecture tree, top nodes in blue as Server Compon

การผสมผสานทั้งสองแบบในโปรเจกต์จริง

การเข้าใจแนวคิดของ Server Component และ Client Component เป็นเพียงจุดเริ่มต้นเท่านั้น สิ่งที่สำคัญกว่าคือการนำทั้งสองแบบมาผสมผสานกันอย่างลงตัวในโปรเจกต์จริง เพราะในแอปพลิเคชันสมัยใหม่แทบไม่มีหน้าจอไหนที่ใช้แค่องค์ประกอบประเภทเดียว โดยทั่วไปแล้วส่วนที่แสดงข้อมูลจากฐานข้อมูลจะเป็น Server Component ขณะที่ส่วนที่ต้องโต้ตอบกับผู้ใช้จะเป็น Client Component และในหลายกรณีเราจะต้องซ้อน Client Component ไว้ข้างใน Server Component หรือในทางกลับกัน การออกแบบโครงสร้าง component tree ให้เหมาะสมจึงเป็นทักษะที่นักพัฒนาต้องฝึกฝน

Composition Pattern คือแนวคิดการจัดวางโครงสร้าง component ที่ช่วยให้เราใช้ประโยชน์จากทั้งสองแบบได้อย่างมีประสิทธิภาพ หลักการสำคัญคือการแบ่งส่วนประกอบของหน้าจอตามหน้าที่การใช้งาน โดย Server Component จะทำหน้าที่ดึงข้อมูลและจัดรูปแบบการแสดงผล ส่วน Client Component จะดูแลส่วนที่ต้องการ interactivity เช่น การคลิก การพิมพ์ หรือการรับ events ต่างๆ การวางตำแหน่งของ component แต่ละประเภทอย่างถูกต้องจะส่งผลให้ bundle size ของ JavaScript ที่ส่งไปยัง client เล็กลง และ page load time เร็วขึ้นอย่างมีนัยสำคัญ

ตัวอย่าง หน้าบล็อกผสม Server + Client

เพื่อให้เห็นภาพชัดเจนขึ้น ลองพิจารณาหน้าบล็อกที่พบได้ทั่วไป หน้านี้ประกอบด้วยส่วนแสดงรายการบทความ ส่วนแสดงเนื้อหาบทความ และส่วนที่ผู้ใช้สามารถกดไลค์หรือแสดงความคิดเห็นได้ ในการสร้างหน้าดังกล่าวเราจะใช้ BlogPage เป็น Server Component ที่ดึงข้อมูลบทความจากฐานข้อมูลโดยไม่ต้องส่ง JavaScript ไปยัง client แต่อย่างใด ในขณะเดียวกัน LikeButton จะเป็น Client Component ที่ต้องใช้ React state เพื่อจัดการการนับไลค์และอัปเดตจำนวนแบบ real-time


// app/blog/[slug]/page.tsx (Server Component)
async function BlogPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}

จากโค้ดด้านบนจะเห็นได้ว่า BlogPage เป็น async function ที่ทำงานบน server โดยตรง นี่คือความสามารถใหม่ของ React Server Components ที่เพิ่มเข้ามาใน Next.js App Router ทำให้ component สามารถทำ data fetching ได้โดยตรงโดยไม่ต้องใช้ useEffect หรือ data fetching library ภายใน component ในขณะเดียวกัน LikeButton เป็น component แยกต่างหากที่ใช้ “use client” directive ซึ่งจะถูก bundle แยกและส่งไปยัง browser เฉพาะส่วนที่จำเป็น


// components/LikeButton.tsx (Client Component)
"use client";

import { useState } from "react";

export function LikeButton({ postId, initialLikes }: Props) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLoading, setIsLoading] = useState(false);

  const handleLike = async () => {
    setIsLoading(true);
    await fetch(`/api/posts/${postId}/like`, { method: "POST" });
    setLikes((prev) => prev + 1);
    setIsLoading(false);
  };

  return (
    <button onClick={handleLike} disabled={isLoading}>
      ❤️ {likes}
    </button>
  );
}

ข้อดีของการแยก LikeButton ออกมาเป็น Client Component คือการที่ส่วนของ logic การจัดการ state และ event handling จะถูกส่งไปยัง client เฉพาะส่วนที่ต้องการ ขณะที่ส่วนการแสดงผลเนื้อหาบทความซึ่งเป็น static content ส่วนใหญ่จะถูก render บน server และส่งเป็น HTML ไปยัง client โดยตรง ทำให้ผู้ใช้เห็นเนื้อหาได้เร็วขึ้น

Pattern ส่ง children ให้ Client Component

อีกรูปแบบหนึ่งที่พบได้บ่อยคือการส่ง Server Component เป็น children ของ Client Component รูปแบบนี้มีประโยชน์มากเมื่อเราต้องการสร้าง layout หรือ wrapper ที่มี interactivity แต่ต้องการให้เนื้อหาภายในถูก render บน server เช่น Card component ที่มี animation เมื่อ hover แต่เนื้อหาภายในมาจากข้อมูลที่ต้องดึงจาก server ตัวอย่างเช่น Card อาจเป็น Client Component ที่ครอบ content ที่เป็น Server Component ทำให้ได้ทั้งความสามารถในการเพิ่ม interactivity และประสิทธิภาพในการ render ข้อมูล


// components/Card.tsx (Client Component)
"use client";

import { ReactNode } from "react";

interface CardProps {
  children: ReactNode;
  title: string;
}

export function Card({ children, title }: CardProps) {
  return (
    <div className="card">
      <h2 className="card-title">{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
}

จากนั้นเราสามารถใช้ Card ร่วมกับ Server Component อื่นๆ ได้อย่างยืดหยุ่น โดยเนื้อหาภายใน Card จะถูกดำเนินการบน server และส่งผลลัพธ์เป็น React Node ไปยัง Client Component รูปแบบนี้เรียกว่า Component Composition และเป็นแนวทางที่ Next.js แนะนำสำหรับการผสมผสานทั้งสองแบบ เพราะช่วยให้เราควบคุมว่าส่วนใดของ component tree ที่ต้องการ interactivity และส่วนใดที่สามารถ render บน server ได้

เลือกใช้ Client หรือ Server Component อย่างไง

การตัดสินใจว่าจะใช้ Server Component หรือ Client Component เป็นทักษะที่ต้องฝึกฝน เพราะไม่มีกฎตายตัวที่ตอบได้ทุกสถานการณ์ หลายครั้งความแตกต่างอยู่ที่บริบทของโปรเจกต์และความต้องการเฉพาะ ในการทำงานจริงเรามักจะพบว่าต้องพิจารณาหลายปัจจัยประกอบกัน เช่น ขนาดของข้อมูลที่ต้องประมวลผล ความถี่ในการอัปเดต state หรือความต้องการ access browser APIs ตารางด้านล่างสรุปสถานการณ์ที่พบบ่อยและแนวทางการเลือกใช้ component ประเภทต่างๆ

สถานการณ์แนะนำเหตุผล
หน้าแสดงรายการสินค้าจากฐานข้อมูลServer Componentข้อมูลมาจาก server ไม่ต้องการ interactivity เป็นพิเศษ ลด JS bundle
ฟอร์มค้นหาสินค้าพร้อม autocompleteClient Componentต้องจัดการ state ขณะพิมพ์ แสดง dropdown แบบ real-time
แสดงข้อมูล user profileServer Componentข้อมูลคงที่ ไม่เปลี่ยนแปลงบ่อย เร็วกว่าดึงจาก client
ปุ่ม toggle theme (dark/light mode)Client Componentต้องใช้ state และ localStorage ซึ่งเป็น browser API
ตารางแสดงข้อมูลพร้อม sort/filterClient Componentต้องจัดการ state สำหรับ sort และ filter ตลอดเวลา
Header navigation แบบ staticServer Componentไม่มี interactivity ส่งเป็น HTML เร็วกว่า
Modal popup สำหรับ loginClient Componentต้องจัดการ open/close state และ form handling
ส่วนแสดงราคาสินค้าคำนวณจาก serverServer Componentดึงข้อมูลราคาจาก API/server โดยตรง ไม่ต้อง fetch ซ้ำ

อย่างไรก็ตาม มีหลักการง่ายๆ ที่ช่วยในการตัดสินใจ กล่าวคือ เริ่มต้นด้วย Server Component เสมอ แล้วเพิ่ม “use client” directive เมื่อจำเป็นต้องใช้ interactivity, state, effects หรือ browser APIs เท่านั้น แนวทางนี้ตรงข้ามกับความเคยชินของนักพัฒนาที่ใช้ React แบบเดิม ซึ่งทุก component ต้องใช้ client-side rendering แต่ใน Next.js 13 ขึ้นไป ค่าเริ่มต้นของ component ใหม่ที่สร้างใน App Router คือ Server Component

สิ่งสำคัญอีกประการคือการเข้าใจว่า Client Component ไม่ได้แยกจาก Server Component โดยสิ้นเชิง ทั้งสองประเภทสามารถทำงานร่วมกันได้อย่างลงตัว ในหลายกรณีเราจะมี Server Component ที่เป็น parent และดึงข้อมูลมาจาก server แล้วส่งข้อมูลนั้นไปให้ Client Component ที่เป็น child เพื่อจัดการส่วนที่ต้องการ interactivity การแยกส่วนให้เหมาะสมจะทำให้ได้ประโยชน์สูงสุดจากทั้งสองแบบ

Performance อันไหนเร็วกว่า

เมื่อพูดถึงประสิทธิภาพ หลายคนอาจสงสัยว่า Server Component กับ Client Component อันไหนเร็วกว่ากัน คำตอบขึ้นอยู่กับบริบทของการวัด ในแง่ของ First Contentful Paint หรือเวลาที่ผู้ใช้เห็นเนื้อหาบนหน้าจอ Server Component มักจะเร็วกว่าเพราะข้อมูลถูกประมวลผลบน server และส่งเป็น HTML ไปยัง browser โดยตรง ทำให้ผู้ใช้เห็นเนื้อหาได้ทันทีโดยไม่ต้องรอ JavaScript bundle download และ execute

ในทางกลับกัน Client Component ต้องดาวน์โหลด JavaScript bundle ไปยัง browser ก่อน จึงจะสามารถ render ได้ ยิ่ง bundle ใหญ่เท่าไหร่ก็ยิ่งใช้เวลานานขึ้นเท่านั้น นี่คือเหตุผลที่การแบ่งส่วนที่ต้องการ interactivity ออกมาจากส่วนที่เป็น static content ช่วยปรับปรุง performance ได้มาก เพราะผู้ใช้จะเห็นเนื้อหาหลักเร็วขึ้น แม้ส่วนที่ต้องโต้ตอบอาจต้องรอ JavaScript อีกสักครู่

สำหรับ Time to Interactive หรือเวลาที่หน้าเว็บพร้อมใช้งานได้ Client Component ที่มี interactivity จะทำให้ผู้ใช้โต้ตอบกับหน้าเว็บได้เร็วกว่า เพราะ JavaScript ที่จำเป็นถูกโหลดไปพร้อมกับ component นั้นๆ และเมื่อ interactivity ทำงานได้ทันทีเมื่อ JavaScript execute เสร็จ ในขณะที่ Server Component ที่รอเฉพาะ HTML จะแสดงผลได้เร็ว แต่ผู้ใช้ยังไม่สามารถโต้ตอบกับ interactive elements ได้จนกว่า JavaScript ที่เกี่ยวข้องจะโหลดเสร็จ

ดังนั้นการเปรียบเทียบว่าอันไหนเร็วกว่าจึงไม่มีคำตอบตายตัว ในแง่ของ user experience โดยรวม Server Component มักจะให้ประสบการณ์ที่ดีกว่าสำหรับเนื้อหาที่คงที่ เพราะผู้ใช้เห็นเนื้อหาได้เร็ว ในขณะที่ Client Component เหมาะสำหรับส่วนที่ต้องโต้ตอบ เพราะให้ feedback ทันทีเมื่อโต้ตอบ การผสมผสานทั้งสองอย่างเหมาะสมจึงเป็นกลยุทธ์ที่ดีที่สุด

Performance comparison infographic split view showing two speedometers, left in blue showing fast se

use server คืออะไร Server Actions

นอกเหนือจาก Server Component แล้ว Next.js 13+ ยังมีคุณสมบัติหนึ่งที่เรียกว่า Server Actions ซึ่งเป็นการทำให้ server-side functions ถูกเรียกใช้จาก client-side ได้โดยตรง โดยการเพิ่ม “use server” directive ที่ท้าย function การทำงานของ Server Actions คล้ายกับการสร้าง API endpoint แต่ใช้งานง่ายกว่ามาก เพราะเราสามารถเรียก function ได้เลยเหมือนเรียก function ปกติ

เพื่อให้เข้าใจความแตกต่างระหว่าง API Routes แบบเดิมกับ Server Actions ลองพิจารณาตารางเปรียบเทียบด้านล่าง

หัวข้อเปรียบเทียบAPI RoutesServer Actions
วิธีการเรียกใช้fetch/axios ไปยัง endpointเรียก function โดยตรง
URL mappingต้องกำหนด URL เองไม่ต้องกำหนด URL

ข้อผิดพลาดที่มือใหม่มักทำ

การเข้าใจแนวคิดของ Server Components และ Client Components นั้นไม่ได้ยากเกินไป แต่ในทางปฏิบัติมือใหม่หลายคนมักจะเจอปัญหาคล้ายๆ กัน ซึ่งทำให้แอปพลิเคชันทำงานผิดพลาดหรือไม่ทำงานตามที่คาดหวัง ส่วนนี้จะพาทุกคนไปดูข้อผิดพลาด 5 ข้อที่พบบ่อยที่สุด พร้อมทั้งแสดงโค้ดที่ผิดและโค้ดที่ถูกต้องเพื่อให้เห็นความแตกต่างอย่างชัดเจน

ข้อผิดพลาดที่ 1: ใส่ “use client” ทุกไฟล์โดยไม่จำเป็น

มือใหม่หลายคนมักคิดว่าทุกอย่างใน Next.js ต้องเป็น Client Component จึงติดป้าย “use client” ไว้ที่ไฟล์ทุกตัว สิ่งนี้ทำให้เสียประโยชน์ของ Server Components ไปโดยใช้เหตุผล เพราะถ้าทุกอย่างเป็น Client Component แล้ว เราก็เทียบเท่ากับการใช้ React แบบดั้งเดิมที่ render ฝั่ง client อย่างเดียว ไม่ต่างอะไรจาก SPA เลย ลองดูตัวอย่างต่อไปนี้ ในไฟล์ผิด เราใส่ “use client” ที่ Homepage component ทั้งที่มันเป็นแค่ static UI ธรรมดา


// ❌ ผิด - ใส่ "use client" ทุกไฟล์
'use client';
 
import React from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import Hero from './components/Hero';
 
export default function Homepage() {
  return (
    <main>
      <Header />
      <Hero />
      <Footer />
    </main>
  );
}

ส่วนไฟล์ที่ถูกต้องนั้น เราจะเอา “use client” ออกเพราะ Homepage ไม่มี interactive elements ใดๆ เลย มันเป็นแค่ layout ธรรมดาที่เอาไว้รวม component ย่อยเข้าด้วยกัน สิ่งสำคัญคือต้องจำไว้ว่า “use client” จำเป็นต้องใส่ก็ต่อเมื่อ component นั้นมีการใช้งาน hooks เช่น useState, useEffect หรือมี event handlers ที่ต้องทำงานฝั่ง client เท่านั้น


// ✅ ถูก - ไม่ใส่ "use client" เพราะเป็น Server Component
import React from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import Hero from './components/Hero';
 
export default function Homepage() {
  return (
    <main>
      <Header />
     Hero />
      <Footer />
    </main>
  );
}

ข้อผิดพลาดที่ 2: ใช้ useState ใน Server Component

นี่เป็นข้อผิดพลาดที่พบบ่อยมากที่สุดข้อหนึ่ง หลายคนยังคงคุ้นเคยกับการใช้ useState ในทุกที่เหมือนกับ React แบบเดิม แต่ใน App Router นั้น Server Component จะทำงานบน server เท่านั้น มันจึงไม่สามารถใช้ hooks ใดๆ ได้เลย ถ้าลองใส่ useState ในไฟล์ที่ไม่มี “use client” จะเกิด error ทันที ในตัวอย่างผิดด้านล่าง เราพยายามใช้ useState เพื่อเก็บค่า counter ใน ProductList component ซึ่งเป็น Server Component


// ❌ ผิด - Server Component ใช้ useState ไม่ได้
import React, { useState } from 'react';
 
export default function ProductList({ products }) {
  const [count, setCount] = useState(0); // Error!
 
  return (
    <div>
      <p>Products: {products.length}</p>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
    </div>
  );
}

วิธีแก้ไขที่ถูกต้องคือแยก Client Component ออกมาต่างหาก ในตัวอย่างถูกต้อง ProductList ยังคงเป็น Server Component ที่รับ products เป็น prop และ render UI ธรรมดา ส่วน interactive button ที่ต้องใช้ useState เราจะแยกออกมาเป็น CounterButton component ที่ต้องใส่ “use client” ไว้ด้านบน การแยกกันอย่างชัดเจนแบบนี้ทำให้เราได้ประโยชน์จาก Server Component สำหรับงานที่ไม่ต้อง interactive และยังคงใช้ Client Component สำหรับงานที่ต้องการ interactivity


// ✅ ถูก - แยก Server Component และ Client Component
'use client';
 
import React, { useState } from 'react';
 
function CounterButton() {
  const [count, setCount] = useState(0);
 
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
 
export default function ProductList({ products }) {
  return (
    <div>
      <p>Products: {products.length}</p>
      <CounterButton />
    </div>
  );
}

ข้อผิดพลาดที่ 3: ลืมใส่ default export

ข้อผิดพลาดนี้แม้จะเป็นเรื่องพื้นฐาน แต่ก็พบได้บ่อยเหมือนกัน โดยเฉพาะกับคนที่เพิ่งย้ายจาก Pages Router มาสู่ App Router ซึ่งใน Pages Router เราสามารถใช้ named export ได้ตามปกติ แต่ใน App Router นั้น Next.js ต้องการ default export เป็นหลัก ถ้าลืมใส่จะเกิด error บอกว่า Cannot find module หรือ Module not found ทันที ในตัวอย่างผิดด้านล่างเราใช้ named export อย่างเดียว


// ❌ ผิด - ใช้ named export แทน default export
'use client';
 
import React from 'react';
 
export function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

สำหรับ Next.js App Router เราต้องเปลี่ยนมาใช้ default export ดังนี้ แม้จะยังสามารถ export named functions อื่นๆ เพิ่มเติมได้ แต่ component หลักที่เป็นตัวแทนของไฟล์นั้นต้องเป็น default export เสมอ


// ✅ ถูก - ใช้ default export
'use client';
 
import React from 'react';
 
export function UserAvatar({ src, alt }) {
  return <img src={src} alt={alt} />;
}
 
export default function UserProfile({ user }) {
  return (
    <div>
      <UserAvatar src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

ข้อผิดพลาดที่ 4: ส่ง function prop ข้าม Server-Client Boundary

ข้อผิดพลาดนี้ซับซ้อนกว่าข้ออื่นเล็กน้อย มันเกี่ยวข้องกับการที่เราส่ง function จาก Server Component ไปยัง Client Component ซึ่งทำไม่ได้เพราะ function ไม่สามารถ serialize ได้ เมื่อ React พยายามส่งข้อมูลระหว่าง server และ client มันต้องแปลงข้อมูลเป็น JSON-like format ซึ่ง function ไม่สามารถทำแบบนั้นได้ ในตัวอย่างผิดด้านล่าง ParentServerComponent ส่ง onClick function ไปยัง InteractiveButton ซึ่งเป็น Client Component สิ่งนี้จะทำให้เกิด error ระหว่าง build


// ❌ ผิด - ส่ง function prop ข้าม boundary
// ParentServerComponent.tsx
'use client';
 
import InteractiveButton from './InteractiveButton';
 
function ParentServerComponent() {
  const handleClick = () => {
    console.log('Clicked!');
  };
 
  return <InteractiveButton onClick={handleClick} />;
}
 
export default ParentServerComponent;

วิธีแก้ไขที่ถูกต้องมีอยู่สองแนวทางหลักๆ แนวทางแรกคือให้ Client Component รับผิดชอบในการสร้าง function เองภายใน เหมือนกับตัวอย่างด้านล่างที่ InteractiveButton สร้าง useState และ handleClick ของตัวเอง แนวทางที่สองคือย้าย ParentServerComponent ไปเป็น Client Component ทั้งหมดถ้ามันต้องการมี logic ที่เกี่ยวกับการ click ในกรณีนี้ถ้า ParentServerComponent ทำอะไรมากกว่าแค่การ click เราอาจต้อง refactor ให้เหมาะสม


// ✅ ถูก - Client Component สร้าง function เอง
'use client';
 
import { useState } from 'react';
 
export default function InteractiveButton({ label }) {
  const [clicked, setClicked] = useState(false);
 
  const handleClick = () => {
    setClicked(!clicked);
    console.log('Button clicked!');
  };
 
  return (
    <button onClick={handleClick} className={clicked ? 'active' : ''}>
      {label}
    </button>
  );
}

ข้อผิดพลาดที่ 5: ใช้ useEffect เพื่อ fetch data โดยตรง

มือใหม่หลายคนยังคงคุ้นเคยกับ pattern เดิมในการ fetch data คือใช้ useEffect ร่วมกับ fetch API แต่ใน Next.js App Router เราไม่ต้องทำแบบนั้นแล้ว เพราะ Server Component สามารถ fetch data ได้โดยตรงโดยไม่ต้องพึ่ง useEffect ในตัวอย่างผิดด้านล่าง ProductList พยายาม fetch data จาก API โดยใช้ useEffect ซึ่งเป็นวิธีที่ทำงานได้แต่ไม่ใช่ best practice ใน App Router เพราะมันทำให้เกิด loading state ที่ซับซ้อนและไม่จำเป็น


// ❌ ผิด - fetch data ด้วย useEffect
'use client';
 
import { useState, useEffect } from 'react';
 
export default function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch('/api/products')
      .then((res) => res.json())
      .then((data) => {
        setProducts(data);
        setLoading(false);
      });
  }, []);
 
  if (loading) return <div>Loading...</div>;
 
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

วิธีที่ถูกต้องคือใช้ Server Component ในการ fetch data โดยตรง ดังนี้ เราสามารถทำ async/await ได้เลยเพราะ component นี้ทำงานบน server เมื่อ data พร้อมแล้วมันจะ render HTML ส่งมาให้ client โดยไม่ต้องรอ loading state อีกต่อไป สิ่งนี้ทำให้ performance ดีขึ้นมากเพราะ user เห็นเนื้อหาทันทีโดยไม่ต้องรอ JavaScript ทำงานก่อน


// ✅ ถูก - fetch data โดยตรงใน Server Component
async function ProductList() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
 
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
 
export default ProductList;

Modern tech illustration comparing four web frameworks in horizontal lane format, React SPA in yello

เปรียบเทียบกับ Framework อื่นๆ

หลังจากเข้าใจพื้นฐานของ Server Components ใน Next.js App Router แล้ว หลายคนอาจสงสัยว่ามันต่างจาก framework อื่นๆ ที่มีอยู่ในตลาดอย่างไร ในส่วนนี้เราจะเปรียบเทียบ Next.js กับ framework ยอดนิยมอื่นๆ ในปัจจุบัน ไม่ว่าจะเป็น React แบบ SPA เดิม, Remix และ Nuxt ซึ่งแต่ละตัวมีแนวทางที่แตกต่างกันในการจัดการเรื่อง server-side rendering และ data fetching

ตารางด้านล่างแสดงการเปรียบเทียบโดยย่อระหว่าง framework ทั้งสี่ตัว โดยเน้นที่ประเด็นหลักๆ ที่เกี่ยวข้องกับการ render และ data fetching ซึ่งเป็นหัวใจสำคัญที่ต้องเข้าใจเมื่อเลือกใช้งาน framework

FrameworkRendering StrategyData FetchingLearning Curve
React SPA (Pages Router)Client-side renderingClient fetch with useEffectต่ำ – เรียนรู้ง่าย
Next.js (App Router)Server Components + selective hydrationServer-side async/awaitปานกลาง – ต้องเข้าใจ boundary
RemixProgressive enhancement with loadersServer loadersปานกลาง – convention-based
Nuxt (Vue)SSR/SSG with composablesServer route handlersปานกลาง – ต้องรู้ Vue composition

ถ้าดูจากตารางแล้วจะเห็นว่า Next.js App Router มีจุดเด่นที่สุดคือเรื่องของการที่ Server Components สามารถ fetch data ได้โดยตรงใน component ซึ่งทำให้โค้ดสะอาดและเข้าใจง่ายกว่าวิธีอื่นๆ ในขณะที่ React SPA นั้นทุกอย่างต้องผ่าน client ทำให้ user ต้องรอ loading ก่อนเห็นเนื้อหา Remix ก็มีแนวคิดที่ดีในเรื่องของ loaders แต่ต้องทำตาม convention ที่ framework กำหนดมากกว่า ส่วน Nuxt นั้นเป็นทางเลือกที่ดีถ้าชอบ Vue แต่มี learning curve ที่คล้ายกัน

เกี่ยวกับผู้เขียน

ITTHIPAT

สวัสดีครับผม อิทธิพัทธ์ (เป้) ชอบหาเทคนิคต่างๆที่ทำให้ชีวิต Programmer ง่ายขึ้น ทั้ง Automate, Library ชอบทำ Blog และ Video ถ้ามีเวลานะ!

ขอบคุณทุกคนที่ติดตาม และอ่านบทความของผมครับ ผมหวังว่าความรู้ที่เขียนขึ้นในเว็บไซต์นี้จะช่วยทุกท่านได้ไม่มากก็น้อย 

Scroll to Top