Next.js Server-side Rendering vs. Static Generation (ฉบับแปล)
Table of contents
เป็นบทความที่แปลและเรียบเรียงใหม่จาก: https://vercel.com/blog/nextjs-server-side-rendering-vs-static-generation
Intro
Next.js เป็น React framework ที่สนับสนุนการทำ pre-rendering แทน Browser render ที่ render จากเริ่มต้นที่ไม่มีอะไรเลย โดย Next.js สามารถทำ pre-rendered HTML ได้ 2 แบบ คือ
- Server-side Rendering (SSR) คือ Next.js จะ pre-renders หน้า page ไปเป็น HTML บน server สำหรับ ทุก request. TTFB (Time to first byte) เป็นวิธีการที่ช้า แต่ข้อมูลจะถูกอัปเดตล่าสุดเสมอ
- Static Generation (SSG) คือ Next.js จะ pre-renders หน้า page ไปเป็น HTML บน server ล่วงหน้าของแต่ละ request ในตอน build time โดย HTMLจะ cached ด้วย CDN และพร้อมใช้งานทันที
Static Generation นั้นมีประสิทธิภาพมาก นั่นก็เพราะว่าการทำ pre-rendering เกิด ขึ้นล่วงหน้า แต่ปัญหาคือเนื้อหาอาจจะเก่าในตอน request
โชคดี มีวิธีแก้ไขปัญหานี้โดยไม่ต้อง rebuilding เมื่อข้อมูลมีการอัปเดต สำหรับ Next.js คุณสามารถใช้ Static Generation สำหรับประสิทธิภาพสูงสุด โดยไม่ต้อง สูญเสียประโยชน์ของ Server-side Rendering
โดยคุณสามารถใช้วิธีเหล่านี้:
- Incremental Static Generation: เพิ่มและอัปเดตหน้า pre-rendered บางส่วน หลังจาก build time
- Client-side Fetching: Generate บางส่วนของหน้าไม่ต้องมีเนื้อหา จากนั้นให้ fetch data ด้วย client-side
เพื่อแสดงตัวอย่างที่ว่านี้ ต่อไปนี้จะใช้แอป Next.js อีคอมเมิร์ซสมมุติเป็นตัวอย่าง
E-commerce Next.js App Example
แอปอีคอมเมิร์ซจะมีหน้าเพจเนื้อหา แต่ละหน้ามีข้อกำหนดข้อมูลที่แตกต่างกันดังต่อไปนี้
- About Us: หน้านี้แสดงข้อมูล บริษัท ซึ่งจะเขียนลงในซอร์สโค้ดของแอปโดยตรง ไม่จำเป็นต้องดึงข้อมูลจากแหล่งอื่นมาประกอบ
- All Products: หน้านี้แสดงรายการสินค้าทั้งหมด ข้อมูลจะถูกดึงมาจากฐานข้อมูล หน้านี้จะมีลักษณะเหมือนกันสำหรับผู้ใช้ทุกคน
- Individual Product: หน้านี้แสดงสินค้าแต่ละรายการ เช่นเดียวกับหน้าสินค้าทั้งหมด โดยข้อมูลจะถูกดึงมาจากฐานข้อมูล และแต่ละหน้าจะมีลักษณะเหมือนกันสำหรับผู้ใช้ทั้งหมด
- Shopping Cart: หน้านี้แสดงตะกร้าสินค้าของผู้ใช้ ข้อมูลจะถูกดึงมาจากฐานข้อมูล หน้านี้จะมีลักษณะแตกต่างกันไปสำหรับผู้ใช้แต่ละคน
Per-page Basis
คุณสมบัติสุดเจ๋งอย่างหนึ่งของ Next.js คือการกำหนดค่าต่อหน้า (per-page configuration) สำหรับการทำ pre-rendering คุณสามารถเลือกวิธีการดึงข้อมูลที่แตกต่างกันสำหรับแต่ละหน้าได้
โดยสำหรับตัวอย่างแอปอีคอมเมิร์ซจะใช้วิธีการต่อไปนี้ ของแต่ละหน้า โดยจะอธิบายวิธีการทำงานดังนี้
- About Us: ใช้ Static Generation โดยไม่ใช้การดึงข้อมูล
- All Products / Individual Product: ใช้ Static Generation ด้วย data และอัปเดตเนื้อหาโดยใช้ Incremental Static Generation
- Shopping Cart: ใช้ Static Generation โดยไม่ใช้การดึงข้อมูล ประกอบกับการทำ Client-side Fetching
About Us Page: Static Generation without Data
สร้างไฟล์ภายใต้ไดเร็กทอรี pages
และเอ็กซ์พอร์ตเฉพาะคอมโพเนนต์
// This page can can be pre-rendered without
// external data: It will be pre-rendered
// into a HTML file at build time.
export default function About() {
return <div>
<h1>About Us</h1>
{/* ... */}
</div>
}
หากหน้าเว็บไม่ต้องการการดึงข้อมูลภายจากนอก หน้านั้นจะแสดงผลล่วงหน้าเป็น HTML โดยอัตโนมัติ ซึ่งในเวลาสร้างเป็นค่าดีฟอลต์สำหรับ Next.js ใช้สิ่งนี้สำหรับหน้า about page ซึ่งไม่ต้องการการดึงข้อมูลใด ๆ
All Products Page: Static Generation with Data
ต่อไปสร้างเพจที่แสดงสินค้าทั้งหมด ซึ่งต้องการดึงข้อมูลจากฐานข้อมูลในขณะ build time ดังนั้นจะใช้ Static Generation
สร้าง page component จากนั้น exports ฟังก์ชัน getStaticProps
โดยฟังก์ชันนี้จะถูกเรียกตอน build time เพื่อ fetch ข้อมูลจากภายนอก และข้อมูลจะถูกใช้เป็น pre-render page component
// This function runs at build time on the build server
export async function getStaticProps() {
return {
props: {
products: await getProductsFromDatabase()
}
}
}
// The page component receives products prop
// from getStaticProps at build time
export default function Products({ products }) {
return (
<>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
)
}
getStaticProps
ทำงานบน build server (Node.js environment) ซึ่งซอร์สโค้ดจะไม่ถูกรวม (not be included) ใน client-side JavaScript bundle นั่นหมายความว่าคุณสามารถเขียน query database โดยตรงในนั้นได้เลย
Individual Product Page: Static Generation with Data
แอปอีคอมเมิร์ซจะมีหน้าสำหรับสินค้าแต่ละรายการ ที่มีวิธีการเรียกขึ้นอยู่กับ id (ตัวอย่างเช่น /products/[id]
)
ใน Next.js นั้นสามารถทำได้ตอน build time ซึ่งใช้ dynamic routes และ getStaticPaths
โดยการสร้างไฟล์เพื่อเรียก products/[id].js
และมี getStaticPaths
โดยจะ return ids ที่เป็นไปได้ออกมา คุณก็จะสามารถ pre-render แต่ละรายการของ product ในตอน build time ได้
จากนั้นคุณสามารถดึงข้อมูลสำหรับสินค้าแต่ละรายการจากฐานข้อมูล โดย สามารถใช้ getStaticProps
เพื่อสำหรับแต่ละ id
ในตอน build time
// pages/products/[id].js
// In getStaticPaths(), you need to return the list of
// ids of product pages (/products/[id]) that you’d
// like to pre-render at build time. To do so,
// you can fetch all products from a database.
export async function getStaticPaths() {
const products = await getProductsFromDatabase()
const paths = products.map((product) => ({
params: { id: product.id }
}))
// fallback: false means pages that don’t have the
// correct id will 404.
return { paths, fallback: false }
}
// params will contain the id for each generated page.
export async function getStaticProps({ params }) {
return {
props: {
product: await getProductFromDatabase(params.id)
}
}
}
export default function Product({ product }) {
// Render product
}
Incremental Static Generation
สมมุติว่าตอนนี้แอปอีคอมเมิร์ซของคุณเติบโตขึ้นอย่างมาก แรก ๆ มีสินค้า 100 รายการ แต่ตอนนี้คุณมี 100,000 รายการ สินค้าได้รับการอัปเดตบ่อยครั้ง สิ่งนี้ก่อให้เกิดปัญหาสองประการ:
- การแสดงผลล่วงหน้า 100,000 หน้าในเวลาสร้างอาจช้ามาก
- เมื่อมีการอัปเดตข้อมูลสินค้า คุณจะต้องแก้ไขเฉพาะหน้าที่ได้รับผลกระทบเท่านั้น โดยไม่อยากจะ rebuild แอปใหม่ทุกครั้งที่มีการแก้ไขสินค้า
ปัญหาทั้งสองนี้สามารถแก้ไขได้โดย Incremental Static Generation โดย Incremental Static Generation ช่วยให้คุณสามารถทำ pre-render ของเพจล่วงหน้าบางส่วนหลังจากเวลา build time นั่นสามารถใช้เพื่อเพิ่มเพจหรืออัปเดตเพจที่แสดงผลล่วงหน้าที่มีอยู่ได้
สิ่งนี้ช่วยให้คุณสามารถใช้ Static Generation เพื่อประสิทธิภาพสูงสุดโดยไม่ต้องเสียประโยชน์จากการแสดงผลฝั่งเซิร์ฟเวอร์
Adding Pages (Fallback)
หากคุณมีสินค้า 100,000 รายการและการแสดงผลหน้าทั้งหมดล่วงหน้าในเวลาสร้างช้าเกินไปคุณสามารถแสดงผลหน้าเว็บล่วงหน้าได้โดยไม่ตั้งใจว่าจะทำ (lazily)
ตัวอย่างเช่นสมมุติว่าสินค้าหนึ่งใน 100,000 รายการเรียกว่าสินค้า X การใช้ Next.js สามารถแสดงผลหน้านี้ล่วงหน้าเมื่อผู้ใช้ร้องขอหน้าสำหรับสินค้า X วิธีการทำงานมีดังนี้
- ผู้ใช้ร้องขอเพจสำหรับสินค้า X
- แต่ยังไม่ได้แสดงหน้านี้ล่วงหน้าไว้ แทนที่จะแสดง 404 Next.js สามารถแสดงเวอร์ชัน "fallback" ของหน้านี้ได้ (เช่นแสดงการโหลด)
- ในเบื้องหลังของ Next.js จะ render หน้าสินค้า X เมื่อเสร็จแล้วหน้าโหลดจะสลับไปที่หน้าสินค้า X
- ในครั้งต่อไปที่มีผู้ร้องขอเพจสำหรับสินค้า X สินค้าของเพจ X ที่ได้ทำการแสดงผลล่วงหน้าไว้แล้วก็จะแสดงผลทันที เช่นเดียวกับการสร้างแบบคงที่ทั่วไป
ในการเปิดใช้งานลักษณะการทำงานนี้คุณสามารถระบุ fallback: true
ใน getStaticPaths
จากนั้นในหน้านั้นคุณสามารถใช้ router.isFallback
เพื่อดูว่าควรแสดงตัวโหลดหรือไม่
// pages/products/[id].js
export async function getStaticProps({ params }) {
// ...
}
export async function getStaticPaths() {
// ...
// fallback: true means that the missing pages
// will not 404, and instead can render a fallback.
return { paths, fallback: true }
}
export default function Product({ product }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
// Render product...
}
สำหรับตัวอย่าง demo นี้ได้ใช้ที่นี่ here ของ Twitter สำหรับ data source
Updating Existing Pages (Incremental Static "Re"generation)
เมื่อมีการอัปเดตข้อมูลสินค้า คุณไม่ต้องการ rebuild แอปใหม่ทั้งหมด คุณต้องการให้มีผลเปลี่ยนแปลงบางเพจเท่านั้น
ตัวอย่างเช่นสมมุติว่าในตอน build time ได้ทำการ pre-rendered หน้าสำหรับสินค้า Y ไว้ล่วงหน้า บางทีกับข้อมูลสินค้า Y ได้รับการอัปเดต
เมื่อใช้ Next.js จะสามารถแสดงผลหน้านี้ล่วงหน้าได้อีกครั้งหลังจากช่วงเวลาหนึ่ง วิธีก ารทำงานมีดังนี้
- Next.js สามารถกำหนด "ระยะหมดเวลา" สำหรับหน้านี้ - ตั้งไว้ที่ 60 วินาที
- อัปเดตข้อมูลสำหรับสินค้า Y แล้ว
- เมื่อมีการร้องขอเพจสำหรับสินค้า Y ผู้ใช้จะเห็นเพจล้าสมัย (out of date) อยู่
- เมื่อมีคำขออื่นใน 60 วินาที หลังจากคำขอก่อนหน้า ผู้ใช้ก็จะเห็นหน้าล้าสมัย (out of date) อยู่ แต่ในเบื้องหลัง Next.js จะ pre-renders หน้านี้ล่วงหน้าอีกครั้ง
- เมื่อการแสดงผลล่วงหน้าเสร็จสิ้น Next.js จะแสดงหน้าที่อัปเดตสำหรับสินค้า Y
วิธีนี้เรียกว่า Incremental Static Regeneration ในกา รเปิดใช้งานคุณสามารถระบุ revalidate: 60
ใน getStaticProps
// pages/products/[id].js
export async function getStaticProps({ params }) {
return {
props: {
product: await getProductFromDatabase(params.id)
},
revalidate: 60
}
}
ประเด็นที่ว่านั้นได้รับแรงบันดาลใจจาก stale-while-revalidate สิ่งนี้ช่วยให้มั่นใจได้ว่าจะมีการให้บริการข้อมูลอย่างแน่นอน และหน้าใหม่จะถูกแสดงหลังจากสร้างสำเร็จแล้วเท่านั้น ผู้ใช้จำนวนน้อยอาจได้รับเนื้อหาที่ล้าสมัยแต่ส่วนใหญ่จะได้รับเนื้อหาล่าสุด และทุกคำร้องขอจะมีความรวดเร็ว เนื่องจาก Next.js ให้บริการเนื้อหาคงที่ (static content) เสมอ
ทั้งการเพิ่มและการอัปเดตเพจได้รับการซัพพอตแล้วอย่างเต็มรูปแบบทั้งจาก next start
และ Vercel Edge Network ของใหม่แกะกล่อง
Shopping Cart Page: Static Generation without Data, Combined with Client-side Fetching
บางหน้าเช่นหน้าตะกร้าสินค้าสามารถแสดงผลล่วงหน้าได้เพียงบางส่วนก่อนการร้องขอ เนื่องจากสินค้าในตะกร้าสินค้าเป็นของเฉพาะสำหรับผู้ใช้แต่ละรายคุณจึงต้องแสดงรายการตามที่ร้องขอเสมอ
คุณอาจคิดว่านี่คือตอนที่คุณจะเลือกใช้การแสดงผลแบบฝั่งเซิร์ฟเวอร์ (Server-side Rendering) แต่ก็ไม่จำเป็นต้องเป็นเช่นนั้น เพื่อประสิทธิภาพที่ดีขึ้นคุณสามารถทำการดึงข้อมูลฝั่งไคลเอ็นต์ (Client-side Fetching) ได้จากการสร้างแบบคงที่ (Static Generation) โดยไม่มีข้อมูลดังนี้:
- แสดงหน้าเว็บล่วงหน้าโดยไม่มีข้อมูลและแสดงสถานะการโหลด (Static Generation)
- จากนั้นดึงข้อมูลและแสดงข้อมูลฝั่งไคลเอ็นต์ (Client-side Fetching)
สำหรับการดึงข้อมูลจากฝั่งไคลเอ็นต์ ขอแนะนำให้ใช้ไลบรารีการดึงข้อมูลที่เรียกว่า SWR จัดการแคชการตรวจสอบความถูกต้อง การติดตาม และอื่น ๆ
import useSWR from 'swr'
function ShoppingCart() {
// fetchAPI is the function to do data fetching
const { data, error } = useSWR('/api/cart', fetchAPI)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>Items in Cart: {data.products.length}</div>
}
Server-side Rendering
หากคุณต้องการใช้การแสดงผลฝั่งเซิร์ฟเวอร์ (แสดงผลเพจล่วงหน้าบนเซิร์ฟเวอร์ในทุกคำขอ) ด้วย Next.js คุณสามารถทำได้ ในการใช้คุณสมบัตินี้คุณสามารถ export ฟังก์ชันที่เรียกว่า getServerSideProps
จากเพจได้เช่นเดียวกับ getStaticProps
นอกจากนี้ยังรองรับการแสดงผลฝั่งเซิร์ฟเวอร์เมื่อ deployed กับ Vercel
อย่างไรก็ตามการใช้การแสดงผลฝั่งเซิร์ฟเวอร์ (Server-side Rendering) จะทำให้คุณไม่ได้รับประโยชน์ของ Static ดังที่ได้กล่าวไว้ข้างต้น เราขอแนะนำให้ลองใช้ Incremental Static Generation หรือ Client-side Fetching และดูว่าเหมาะสมกับความต้องการของคุณหรือไม่
Also: Writing Data
การดึงข้อมูล (Fetching data) เป็นเพียงครึ่งหนึ่งของความสมบูรณ์ของแอป โดยอาจต้องเขียนข้อมูลกลับไปยังแหล่งข้อมูลของคุณ สำหรับแอปอีคอมเมิร์ซของเราการเพิ่มสินค้าลงในตะกร้าสินค้าเป็นตัวอย่างที่ดีสำหรับหัวข้อนี้
Next.js มีคุณลักษณะที่เรียกว่า API Routes สำหรับวัตถุประสงค์ในการใช้คุณสมบัตินี้คุณสามารถสร้างไฟล์ภายในไดเร็กทอรี pages/api
ซึ่งสร้างจุดของ API (endpoint) ที่สามารถใช้เพื่อเปลี่ยนแหล่งข้อมูล ตัวอย่างเช่นสามารถสร้าง pages/api/cart.js
ซึ่งรับพารามิเตอร์การค้นหา productId
และเพิ่มสินค้านั้นลงในรถเข็น
ภายใน API routes จะ export ตัวจัดการคำขอซึ่งรับคำขอและส่งคืนการตอบกลับเป็น json
export default async (req, res) => {
const response = await fetch(`https://.../cart`, {
body: JSON.stringify({
productId: req.query.productId
}),
headers: {
Authorization: `Token ${process.env.YOUR_API_KEY}`,
'Content-Type': 'application/json'
},
method: 'POST'
})
const { products } = await response.json()
return res.status(200).json({ products })
};
API routes ช่วยให้เราเขียนไปยังแหล่งข้อมูลภายนอกได้อย่างปลอดภัย การใช้ตัวแปรสภาพแวดล้อม (environment variables) เราสามารถเพิ่มค่าคีย์ต่าง ๆ สำหรับการพิสูจน์ตัวตนโดยไม่เปิดเผยค่าฝั่งไคลเอ็นต์
API routes สามารถ deployed เป็นฟังก์ชั่นไร้เซิร์ฟเวอร์ (Serverless Functions) (ซึ่งเป็นค่าเริ่มต้นเมื่อคุณ deploy กับ Vercel)
Conclusion
ด้วย Next.js คุณสามารถใช้ Static Generation เพื่อประสิทธิภาพสูงสุด โดยไม่ต้องเสียประโยชน์จากการแสดงผลฝั่งเซิร์ฟเวอร์ สำหรับข้อมูลเพิ่มเติมดูได้จากเอกสาร Next.js