Skip to main content

Next.js Server-side Rendering vs. Static Generation (ฉบับแปล)

Kongvut Sangkla

เป็นบทความที่แปลและเรียบเรียงใหม่จาก: 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

info

โชคดี มีวิธีแก้ไขปัญหานี้โดยไม่ต้อง 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 และเอ็กซ์พอร์ตเฉพาะคอมโพเนนต์

pages/about.js

// 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>
}
note

หากหน้าเว็บไม่ต้องการการดึงข้อมูลภายจากนอก หน้านั้นจะแสดงผลล่วงหน้าเป็น 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

pages/products.js
// 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>
</>
)
}
note

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.js
// 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 นั่นสามารถใช้เพื่อเพิ่มเพจหรืออัปเดตเพจที่แสดงผลล่วงหน้าที่มีอยู่ได้

note

สิ่งนี้ช่วยให้คุณสามารถใช้ Static Generation เพื่อประสิทธิภาพสูงสุดโดยไม่ต้องเสียประโยชน์จากการแสดงผลฝั่งเซิร์ฟเวอร์

Adding Pages (Fallback)

หากคุณมีสินค้า 100,000 รายการและการแสดงผลหน้าทั้งหมดล่วงหน้าในเวลาสร้างช้าเกินไปคุณสามารถแสดงผลหน้าเว็บล่วงหน้าได้โดยไม่ตั้งใจว่าจะทำ (lazily)

ตัวอย่างเช่นสมมุติว่าสินค้าหนึ่งใน 100,000 รายการเรียกว่าสินค้า X การใช้ Next.js สามารถแสดงผลหน้านี้ล่วงหน้าเมื่อผู้ใช้ร้องขอหน้าสำหรับสินค้า X วิธีการทำงานมีดังนี้

  1. ผู้ใช้ร้องขอเพจสำหรับสินค้า X
  2. แต่ยังไม่ได้แสดงหน้านี้ล่วงหน้าไว้ แทนที่จะแสดง 404 Next.js สามารถแสดงเวอร์ชัน "fallback" ของหน้านี้ได้ (เช่นแสดงการโหลด)
  3. ในเบื้องหลังของ Next.js จะ render หน้าสินค้า X เมื่อเสร็จแล้วหน้าโหลดจะสลับไปที่หน้าสินค้า X
  4. ในครั้งต่อไปที่มีผู้ร้องขอเพจสำหรับสินค้า X สินค้าของเพจ X ที่ได้ทำการแสดงผลล่วงหน้าไว้แล้วก็จะแสดงผลทันที เช่นเดียวกับการสร้างแบบคงที่ทั่วไป

ในการเปิดใช้งานลักษณะการทำงานนี้คุณสามารถระบุ fallback: true ใน getStaticPaths จากนั้นในหน้านั้นคุณสามารถใช้ router.isFallback เพื่อดูว่าควรแสดงตัวโหลดหรือไม่

pages/products.js
// 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...
}
note

สำหรับตัวอย่าง demo นี้ได้ใช้ที่นี่ here ของ Twitter สำหรับ data source

Updating Existing Pages (Incremental Static "Re"generation)

เมื่อมีการอัปเดตข้อมูลสินค้า คุณไม่ต้องการ rebuild แอปใหม่ทั้งหมด คุณต้องการให้มีผลเปลี่ยนแปลงบางเพจเท่านั้น

ตัวอย่างเช่นสมมุติว่าในตอน build time ได้ทำการ pre-rendered หน้าสำหรับสินค้า Y ไว้ล่วงหน้า บางทีกับข้อมูลสินค้า Y ได้รับการอัปเดต

เมื่อใช้ Next.js จะสามารถแสดงผลหน้านี้ล่วงหน้าได้อีกครั้งหลังจากช่วงเวลาหนึ่ง วิธีการทำงานมีดังนี้

  1. Next.js สามารถกำหนด "ระยะหมดเวลา" สำหรับหน้านี้ - ตั้งไว้ที่ 60 วินาที
  2. อัปเดตข้อมูลสำหรับสินค้า Y แล้ว
  3. เมื่อมีการร้องขอเพจสำหรับสินค้า Y ผู้ใช้จะเห็นเพจล้าสมัย (out of date) อยู่
  4. เมื่อมีคำขออื่นใน 60 วินาที หลังจากคำขอก่อนหน้า ผู้ใช้ก็จะเห็นหน้าล้าสมัย (out of date) อยู่ แต่ในเบื้องหลัง Next.js จะ pre-renders หน้านี้ล่วงหน้าอีกครั้ง
  5. เมื่อการแสดงผลล่วงหน้าเสร็จสิ้น Next.js จะแสดงหน้าที่อัปเดตสำหรับสินค้า Y
note

วิธีนี้เรียกว่า Incremental Static Regeneration ในการเปิดใช้งานคุณสามารถระบุ revalidate: 60 ใน getStaticProps

pages/products.js
// 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) โดยไม่มีข้อมูลดังนี้:

  1. แสดงหน้าเว็บล่วงหน้าโดยไม่มีข้อมูลและแสดงสถานะการโหลด (Static Generation)
  2. จากนั้นดึงข้อมูลและแสดงข้อมูลฝั่งไคลเอ็นต์ (Client-side Fetching)

สำหรับการดึงข้อมูลจากฝั่งไคลเอ็นต์ ขอแนะนำให้ใช้ไลบรารีการดึงข้อมูลที่เรียกว่า SWR จัดการแคชการตรวจสอบความถูกต้อง การติดตาม และอื่น ๆ

pages/shopping-cart.js
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

pages/api/cart.js
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

Reference

https://vercel.com/blog/nextjs-server-side-rendering-vs-static-generation#shopping-cart-page-static-generation-without-data,-combined-with-client-side-fetching

Loading...