React, Next.js และ TypeScript กลายเป็นชุดเทคโนโลยีมาตรฐานสำหรับเว็บแอปพลิเคชันระดับมืออาชีพ React ให้โมเดลแบบคอมโพเนนต์ Next.js เพิ่มระบบ routing การเรนเดอร์ และการดึงข้อมูล ส่วน TypeScript จับข้อผิดพลาดได้ทั้งกลุ่มก่อนที่โค้ดจะรันเสียอีก หากใช้อย่างถูกต้อง จะได้เว็บที่เร็วสำหรับผู้เข้าชมและดูแลง่ายสำหรับนักพัฒนา
แต่ถ้าใช้อย่างไม่ระวัง ชุดเทคโนโลยีเดียวกันนี้ก็สร้างหน้าเว็บที่ช้าและเต็มไปด้วย JavaScript หลายเมกะไบต์ บทความนี้อธิบายแนวคิดที่สร้างความแตกต่าง พร้อมโค้ดที่นำไปใช้ในโปรเจกต์ Next.js App Router ได้ทันที
ทำไมชุดเทคโนโลยีนี้จึงกลายเป็นมาตรฐาน
- React อธิบายหน้าจอเป็นต้นไม้ของคอมโพเนนต์ที่นำกลับมาใช้ซ้ำได้ ปุ่ม การ์ด หรือทั้งหน้าเป็นฟังก์ชันของข้อมูล ทำให้หน้าจอคาดเดาได้และทดสอบง่าย
- Next.js เปลี่ยน React ให้เป็นเฟรมเวิร์กเต็มรูปแบบ ทั้ง routing ตามไฟล์ การเรนเดอร์ฝั่งเซิร์ฟเวอร์ การแคช การปรับแต่งรูปภาพและฟอนต์ และกระบวนการ build สำหรับ production
- TypeScript เพิ่ม static type ให้ JavaScript เอดิเตอร์รู้โครงสร้างของทุกอ็อบเจกต์ การ refactor ปลอดภัย และข้อผิดพลาดตอนรันจำนวนมากกลายเป็นข้อผิดพลาดตอนคอมไพล์
เมื่อรวมกัน ทั้งสามครอบคลุมทุกอย่างตั้งแต่เว็บไซต์การตลาดไปจนถึงแดชบอร์ดที่ซับซ้อน ด้วยภาษาเดียวทั้งฝั่ง frontend และเซิร์ฟเวอร์
Server Components เปลี่ยนค่าเริ่มต้น
ใน Next.js App Router ทุกคอมโพเนนต์เป็น React Server Component เว้นแต่คุณจะเลือกเป็นอย่างอื่น Server Components ทำงานบนเซิร์ฟเวอร์เท่านั้น อ่านข้อมูลจากฐานข้อมูลหรือเรียก API ภายในได้โดยตรง และ JavaScript ของมันไม่เคยถูกส่งไปที่เบราว์เซอร์ มีเพียงผลลัพธ์ที่เรนเดอร์แล้วเท่านั้น
คุณใส่ directive 'use client' เฉพาะคอมโพเนนต์ที่ต้องมีการโต้ตอบ เช่น state, effect, event handler หรือ API ของเบราว์เซอร์ แนวทางที่ดีที่สุดคือทำให้ client component มีขนาดเล็ก และวางไว้ที่ปลายกิ่งของต้นไม้:
// app/blog/page.tsx: a Server Component (the default in the App Router)
import { getPosts } from '@/lib/posts'
import { LikeButton } from './like-button'
export default async function BlogPage() {
const posts = await getPosts() // runs on the server, so no extra API route is needed
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
<LikeButton />
</li>
))}
</ul>
)
}// app/blog/like-button.tsx: only this small component ships JavaScript
'use client'
import { useState } from 'react'
export function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button type="button" aria-pressed={liked} onClick={() => setLiked((value) => !value)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}รายการ การเข้าถึงข้อมูล และ markup ไม่เพิ่มภาระให้เบราว์เซอร์เลย มีแค่ปุ่มที่ต้อง hydrate สำหรับหน้าที่เน้นเนื้อหา วิธีนี้มักตัด JavaScript ฝั่ง client ออกไปได้เกือบทั้งหมด
เลือกกลยุทธ์การเรนเดอร์ให้ถูกต้อง
Next.js ให้คุณกำหนดได้ในแต่ละ route ว่าจะสร้าง HTML เมื่อไร การเลือกให้ถูกคือการตัดสินใจด้านประสิทธิภาพที่สำคัญที่สุดที่คุณทำ:
- Static rendering: สร้าง HTML ตอน build แล้วเสิร์ฟจากแคชหรือ CDN เหมาะกับหน้าการตลาดและเอกสาร
- Incremental Static Regeneration (ISR): หน้า static ที่รีเฟรชเมื่อครบเวลาที่กำหนดหรือเมื่อสั่งด้วย
revalidatePath()เหมาะกับบล็อก แคตตาล็อก และหน้าที่ดึงเนื้อหาจาก CMS - Dynamic rendering: สร้าง HTML ทุกครั้งที่มีคำขอ จำเป็นสำหรับหน้าที่ปรับตามผู้ใช้ แดชบอร์ด และทุกอย่างที่ขึ้นกับ cookie
- Streaming: ครอบส่วนที่ช้าของหน้าด้วย
<Suspense>เพื่อให้โครงหน้าแสดงทันที และข้อมูลที่ช้าค่อยๆ สตรีมตามมาเมื่อพร้อม
route บล็อกแบบ dynamic ที่ถูกแคชและรีเฟรชทุกห้านาที พร้อม metadata ด้าน SEO แยกตามบทความ มีหน้าตาแบบนี้ โปรดสังเกตว่าใน Next.js เวอร์ชันปัจจุบัน params เป็น Promise:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPost } from '@/lib/posts'
type Props = { params: Promise<{ slug: string }> }
// Serve the cached page, regenerating it at most every five minutes
export const revalidate = 300
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
openGraph: { type: 'article', title: post.title, images: [post.coverUrl] },
}
}
export default async function PostPage({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound() // renders the 404 page with the correct status code
return (
<article>
<h1>{post.title}</h1>
{/* … */}
</article>
)
}TypeScript ที่คุ้มค่ากับการลงทุน
เปิดโหมด strict ตั้งแต่ commit แรก เพราะการมาเปิดภายหลังเจ็บปวดมาก noUncheckedIndexedAccess ก็ควรเปิดเช่นกัน เพราะมันบังคับให้คุณจัดการกรณีที่สมาชิกของอาร์เรย์หรือรายการใน map ไม่มีอยู่จริง:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}อย่าลืมว่าไทป์จะหายไปตอนรัน ข้อมูลที่ข้ามขอบเขตของระบบ (อินพุตจากฟอร์ม ผลตอบกลับจาก API พารามิเตอร์ใน URL ตัวแปรสภาพแวดล้อม) ต้องถูกตรวจสอบความถูกต้อง ไม่ใช่แค่กำหนดไทป์ ไลบรารี schema อย่าง zod ให้ทั้งสองอย่างจากนิยามเดียว:
import { z } from 'zod'
export const PostSchema = z.object({
slug: z.string().regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(200),
publishedAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
})
// One source of truth for the runtime check and the compile-time type
export type Post = z.infer<typeof PostSchema>
export function parsePost(input: unknown): Post {
return PostSchema.parse(input) // throws a descriptive error for invalid data
}สร้างโมเดลของสถานะอย่างชัดเจนด้วย discriminated union เช่น { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; data: Post[] } แล้วคอมไพเลอร์จะบังคับให้ทุกคอมโพเนนต์จัดการสถานะกำลังโหลดและสถานะข้อผิดพลาด แทนที่จะคิดว่าทุกอย่างราบรื่นเสมอ
แก้ไขข้อมูลด้วย Server Actions
Server Actions ให้ฟอร์มเรียกฟังก์ชันบนเซิร์ฟเวอร์ได้โดยตรง โดยไม่ต้องเขียน API route เอง สะดวกก็จริง แต่เบื้องหลังมันคือ HTTP endpoint สาธารณะ จงปฏิบัติกับทุก action เหมือน API: ตรวจสอบอินพุต ตรวจสอบการยืนยันตัวตนและสิทธิ์บนเซิร์ฟเวอร์ และส่งข้อผิดพลาดที่มีโครงสร้างกลับไปให้ฟอร์มแสดงผล การซ่อนปุ่มบนหน้าจอไม่ใช่ความปลอดภัย
เช็กลิสต์ด้านประสิทธิภาพและการเข้าถึง
- ใช้
next/imageพร้อมแอตทริบิวต์sizesที่ถูกต้อง เพื่อไม่ให้มือถือโหลดรูปขนาดเดสก์ท็อป และกำหนดpriorityให้เฉพาะรูปหลักที่อยู่ส่วนบนของหน้าจอ - โหลดฟอนต์ด้วย
next/fontเพื่อโฮสต์ฟอนต์เองและหลีกเลี่ยงการขยับของเลย์เอาต์ - ดึงข้อมูลที่ไม่ขึ้นต่อกันแบบขนานด้วย
Promise.allแทนการawaitต่อกันทีละขั้น - ทำให้ client component เล็กและอยู่ปลายกิ่ง และโหลดวิดเจ็ตหนักๆ อย่างแผนที่ กราฟ และเอดิเตอร์แบบ lazy
- ติดตาม Core Web Vitals บน production ระดับ "ดี" หมายถึง Largest Contentful Paint ต่ำกว่า 2.5 วินาที Interaction to Next Paint ต่ำกว่า 200 มิลลิวินาที และ Cumulative Layout Shift ต่ำกว่า 0.1 ที่เปอร์เซ็นไทล์ที่ 75
- ใช้ HTML เชิงความหมาย (
<nav>,<main>,<article>,<button>จริงๆ) ใส่ label ให้ทุกช่องในฟอร์ม ทำให้สถานะโฟกัสมองเห็นได้เสมอ และตรวจสอบคอนทราสต์ของสี
ประเด็นสำคัญ
- ใช้ Server Components เป็นค่าเริ่มต้น และเพิ่ม
'use client'เฉพาะจุดที่ต้องโต้ตอบ - เลือกกลยุทธ์การเรนเดอร์ราย route: static เมื่อทำได้ ISR สำหรับเนื้อหา และ dynamic เมื่อจำเป็นเท่านั้น
- ใช้ TypeScript แบบ strict ควบคู่กับการตรวจสอบข้อมูลตอนรันที่ทุกขอบเขตของระบบ
- วัดประสิทธิภาพจากผู้ใช้จริง และถือว่าการเข้าถึงได้เป็นส่วนหนึ่งของนิยามคำว่า "เสร็จ"
สรุป
ชุดเทคโนโลยี React ยุคใหม่ให้รางวัลกับทีมที่เข้าใจว่าโค้ดของตนทำงานอยู่ที่ไหน การเรนเดอร์บนเซิร์ฟเวอร์เป็นค่าเริ่มต้น การส่ง JavaScript เฉพาะสำหรับการโต้ตอบจริง และการบังคับใช้ไทป์กับการตรวจสอบข้อมูลที่ขอบเขตของระบบ จะทำให้แอปของคุณโหลดเร็ว ติดอันดับดี และยังแก้ไขได้ง่ายแม้ระบบจะเติบโตขึ้น
อ่านเพิ่มเติม
- #Next.js
- #React
- #TypeScript
- #Web Performance




