ทำไม Partial Prerendering ถึงเปลี่ยนเกมการ Rendering ใน Next.js
ถ้าคุณเคยสร้างเว็บด้วย Next.js มาก่อน น่าจะคุ้นเคยกับทางเลือกในการ render หน้าเว็บ 2 แบบหลักๆ — Static Rendering (สร้าง HTML ตอน build time ทั้งหน้า) หรือ Dynamic Rendering (สร้าง HTML ใหม่ทุกครั้งที่มี request) เลือก static ก็ได้ความเร็วสุดๆ แต่แสดงข้อมูล personalized ไม่ได้ เลือก dynamic ก็ได้ข้อมูลสดใหม่ แต่ต้องแลกกับ performance ที่ช้าลง
แต่ในโลกจริง หน้าเว็บส่วนใหญ่ไม่ได้เป็น static ล้วนหรือ dynamic ล้วนหรอก มันเป็นทั้งสองอย่างผสมกัน
ลองนึกภาพหน้าสินค้า E-Commerce ที่มีรายละเอียดสินค้า (static) อยู่ข้างๆ ตะกร้าสินค้าของผู้ใช้ (dynamic) หรือหน้า Dashboard ที่มี sidebar เป็น static แต่ content หลักเปลี่ยนตลอดเวลา ปัญหานี้เจอกันบ่อยมาก
ใน Next.js เวอร์ชันเก่า ถ้าหน้าเว็บมีแม้แค่ส่วนเดียวที่ต้องเป็น dynamic ทั้งหน้าก็จะถูก render แบบ dynamic ทั้งหมด ผู้ใช้ต้องรอ server ประมวลผลทั้งหน้า แม้ว่า 90% ของเนื้อหาจะเป็น static ที่ไม่เปลี่ยนแปลงก็ตาม ซึ่งพูดตรงๆ ว่า... มันค่อนข้างสิ้นเปลือง
Partial Prerendering (PPR) ใน Next.js 16 เข้ามาแก้ปัญหานี้โดยตรง ให้คุณ prerender ส่วนที่เป็น static ล่วงหน้า แล้วค่อย stream ส่วน dynamic เข้ามาทีหลัง ทั้งหมดนี้เกิดขึ้นใน HTTP response เดียว ผู้ใช้เห็นเนื้อหาหลักทันทีโดยไม่ต้องรอ
PPR คืออะไร? ทำความเข้าใจหลักการทำงาน
Partial Prerendering (PPR) คือกลยุทธ์การ render ที่ผสมผสาน static content กับ dynamic content ภายใน route เดียวกัน โดยไม่ต้องเลือกอย่างใดอย่างหนึ่ง ถ้าจะพูดง่ายๆ มันคือ rendering model รูปแบบที่ 3 ที่เพิ่มเข้ามานอกเหนือจาก SSG และ SSR แบบเดิม
หลักการทำงานของ PPR มี 3 ขั้นตอนหลัก:
- Build Time — Next.js สร้าง static shell ของหน้าเว็บล่วงหน้า ส่วนที่ไม่ต้องการข้อมูล dynamic จะถูก prerender เป็น HTML พร้อมใช้งาน
- Request Time — เมื่อผู้ใช้เข้าถึงหน้าเว็บ server ส่ง static shell ออกไปทันที พร้อมกับเริ่ม render ส่วน dynamic แบบ parallel
- Streaming — ส่วน dynamic จะถูก stream เข้ามาแทนที่ fallback UI ที่แสดงอยู่ ทั้งหมดนี้เกิดขึ้นภายใน HTTP response เดียว ไม่ต้อง roundtrip เพิ่ม
ตรงนี้สำคัญนะ — สิ่งที่ทำให้ PPR พิเศษกว่าการใช้ Streaming/Suspense แบบเดิมคือ ด้วย Streaming/Suspense ปกติ ทั้งหน้าจะถูก render ตอน request time ทุกครั้ง แต่ด้วย PPR ส่วน static จะถูก prerender ตอน build time จริงๆ ทำให้ส่งถึงผู้ใช้ได้เร็วกว่ามาก เพราะสามารถ serve จาก CDN edge ได้เลย
เปิดใช้งาน PPR ใน Next.js 16 ด้วย Cache Components
ข่าวดีคือการเปิดใช้งาน PPR ใน Next.js 16 ไม่ได้ยุ่งยากอะไรเลย ทำได้ผ่าน flag cacheComponents ใน next.config.ts ซึ่งเป็น flag ตัวเดียวกันกับที่เปิดใช้ Cache Components:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true, // เปิดทั้ง PPR และ Cache Components
}
export default nextConfig
แค่นี้เอง จริงๆ เมื่อเปิด cacheComponents แล้ว พฤติกรรมของ Next.js จะเปลี่ยนไปดังนี้:
- Data fetching ทั้งหมดจะเป็น dynamic by default — ไม่มีการ cache อัตโนมัติอีกต่อไป
- ถ้าต้องการ cache ส่วนไหน ต้องใช้
'use cache'directive บอกตรงๆ - ส่วนที่ถูก mark ด้วย
'use cache'จะถูกรวมเข้าใน static shell - ส่วนที่เป็น dynamic จะถูกห่อด้วย
<Suspense>เพื่อกำหนด fallback UI
ข้อควรทราบ: เมื่อเปิด cacheComponents route segment config แบบเดิมอย่าง export const dynamic = 'force-dynamic' จะใช้ไม่ได้อีกต่อไป เพราะถูกแทนที่ด้วยระบบ static/dynamic rendering แบบใหม่ที่ใช้ use cache และ <Suspense> ถ้าคุณมีโค้ดเก่าที่ใช้ route segment config อยู่ ก็ต้อง migrate มาใช้ระบบใหม่
Suspense — หัวใจสำคัญของ PPR
React <Suspense> คือกลไกหลักที่ PPR ใช้ในการแยก static กับ dynamic content ออกจากกัน ง่ายๆ เลยคือ — ทุกอย่างที่อยู่นอก Suspense boundary จะถูก prerender เป็น static shell ส่วนทุกอย่างที่อยู่ข้างใน Suspense จะถูก defer ไปทำตอน request time
มาดูตัวอย่างกัน:
// app/page.tsx
import { Suspense } from 'react'
import { ProductList } from '@/components/ProductList'
import { UserCart } from '@/components/UserCart'
import { CartSkeleton } from '@/components/skeletons'
export default function HomePage() {
return (
<main>
{/* ส่วนนี้ถูก prerender เป็น static shell */}
<h1>ยินดีต้อนรับสู่ร้านค้าออนไลน์</h1>
<ProductList />
{/* ส่วนนี้เป็น dynamic — stream เข้ามาทีหลัง */}
<Suspense fallback={<CartSkeleton />}>
<UserCart />
</Suspense>
</main>
)
}
ในตัวอย่างนี้ <h1> และ <ProductList /> จะถูก prerender เป็น HTML ตอน build time — ผู้ใช้เห็นทันทีเมื่อเข้าหน้านี้ ส่วน <UserCart /> ที่ต้องอ่าน cookies เพื่อระบุตัวผู้ใช้จะถูก render ตอน request time โดยจะเห็น <CartSkeleton /> ก่อน แล้วจึงถูกแทนที่ด้วยตะกร้าจริง
สิ่งที่ทำให้ component เป็น dynamic
จุดนี้สำคัญมาก — การห่อ component ด้วย <Suspense> ไม่ได้ทำให้มันเป็น dynamic โดยตัวของมันเอง สิ่งที่ทำให้ component ต้องเป็น dynamic คือการเรียกใช้ Dynamic APIs พวกนี้:
cookies()— อ่าน cookies จาก requestheaders()— อ่าน HTTP headerssearchParams— อ่าน URL query parametersconnection()— รอให้ connection พร้อม- fetch โดยไม่ cache —
fetch(url, { cache: 'no-store' })
ถ้า component ไหนเรียกใช้ API เหล่านี้แต่ไม่ได้ห่อด้วย <Suspense> หรือ mark ด้วย use cache จะเจอ error ว่า "Uncached data was accessed outside of Suspense" ตอน dev หรือ build ซึ่งจริงๆ ถือว่าเป็นเรื่องดี เพราะช่วยจับ bug ก่อนขึ้น production
ตัวอย่างจริง: สร้างหน้า Product ด้วย PPR
ทีนี้มาดูตัวอย่างที่ใกล้เคียงกับการใช้งานจริงมากขึ้นกันบ้าง สมมติเราสร้างหน้าสินค้าที่มีทั้ง static content และ personalized content:
1. Data Fetching Functions
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'
import { cookies } from 'next/headers'
// ข้อมูลสินค้า — cache ได้ เพราะไม่ขึ้นกับ user
export async function getProduct(id: string) {
'use cache'
cacheLife('hours')
cacheTag(`product-${id}`)
const res = await fetch(`https://api.example.com/products/${id}`)
return res.json()
}
// รีวิวสินค้า — cache ได้ เพราะเหมือนกันทุก user
export async function getReviews(productId: string) {
'use cache'
cacheLife('minutes')
cacheTag(`reviews-${productId}`)
const res = await fetch(`https://api.example.com/reviews?product=${productId}`)
return res.json()
}
// สินค้าแนะนำ — dynamic เพราะขึ้นกับ user
export async function getRecommendations() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const res = await fetch(`https://api.example.com/recommendations?user=${userId}`, {
cache: 'no-store',
})
return res.json()
}
สังเกตไหมว่า getProduct กับ getReviews มี 'use cache' directive ข้างบน ส่วน getRecommendations เรียก cookies() ซึ่งทำให้มันเป็น dynamic โดยอัตโนมัติ
2. หน้า Product Page
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { getProduct, getReviews, getRecommendations } from '@/lib/data'
import { ReviewsSkeleton, RecommendationsSkeleton } from '@/components/skeletons'
interface Props {
params: Promise<{ id: string }>
}
export default async function ProductPage({ params }: Props) {
const { id } = await params
const product = await getProduct(id) // cached — รวมใน static shell
return (
<main>
{/* ส่วน Static Shell — prerender ตอน build */}
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>ราคา: ฿{product.price.toLocaleString()}</span>
<img src={product.imageUrl} alt={product.name} />
</section>
{/* ส่วน Dynamic — stream เข้ามาแบบ parallel */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewsSection productId={id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<RecommendationsSection />
</Suspense>
</main>
)
}
async function ReviewsSection({ productId }: { productId: string }) {
const reviews = await getReviews(productId) // cached แต่ revalidate บ่อย
return (
<section>
<h2>รีวิวจากลูกค้า ({reviews.length})</h2>
<ul>
{reviews.map((review: any) => (
<li key={review.id}>
<strong>{review.author}</strong>: {review.comment}
</li>
))}
</ul>
</section>
)
}
async function RecommendationsSection() {
const items = await getRecommendations() // dynamic — ขึ้นกับ user
return (
<section>
<h2>สินค้าแนะนำสำหรับคุณ</h2>
<div>
{items.map((item: any) => (
<div key={item.id}>
<p>{item.name}</p>
<span>฿{item.price.toLocaleString()}</span>
</div>
))}
</div>
</section>
)
}
ในตัวอย่างนี้ หน้า product page ถูกแบ่งออกเป็น 3 ส่วนชัดเจน:
- Product Info — cached ด้วย
use cache→ รวมอยู่ใน static shell ผู้ใช้เห็นทันที - Reviews — cached เหมือนกันแต่ revalidate ทุกไม่กี่นาที → อยู่ใน static shell เช่นกัน แต่อัปเดตบ่อยกว่า
- Recommendations — dynamic เพราะใช้ cookies → ห่อด้วย Suspense และ stream เข้ามาตอน request
ผลลัพธ์ที่ได้คือผู้ใช้เห็นข้อมูลสินค้าและรีวิวภายในเสี้ยววินาที ส่วนสินค้าแนะนำจะโหลดตามมาอย่างราบรื่น ประสบการณ์ใช้งานดีขึ้นเยอะเลย
ใช้ Nested Suspense สำหรับ Progressive Loading
ไม่ใช่ทุก dynamic content จะโหลดเร็วเท่ากัน (อันนี้แน่นอนอยู่แล้ว) คุณสามารถใช้ Nested Suspense เพื่อสร้างประสบการณ์ progressive loading ที่ content ถูกเปิดเผยทีละชั้นตามความเร็วของข้อมูลแต่ละส่วน:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { DashboardStats } from '@/components/DashboardStats'
import { RecentOrders } from '@/components/RecentOrders'
import { ActivityFeed } from '@/components/ActivityFeed'
export default function DashboardPage() {
return (
<main>
<h1>แดชบอร์ด</h1>
{/* ข้อมูลสรุป — โหลดเร็ว */}
<Suspense fallback={<div>กำลังโหลดสถิติ...</div>}>
<DashboardStats />
{/* ซ้อน Suspense — รอข้อมูลออเดอร์ */}
<Suspense fallback={<div>กำลังโหลดออเดอร์ล่าสุด...</div>}>
<RecentOrders />
</Suspense>
</Suspense>
{/* Activity Feed — โหลดแยกอิสระ */}
<Suspense fallback={<div>กำลังโหลดกิจกรรมล่าสุด...</div>}>
<ActivityFeed />
</Suspense>
</main>
)
}
ด้วยวิธีนี้ DashboardStats จะแสดงผลก่อน แล้ว RecentOrders จะตามมา ขณะที่ ActivityFeed โหลดแบบ parallel แยกอิสระ ผู้ใช้ไม่ต้องนั่งจ้องหน้าจอเปล่ารอ
เปรียบเทียบ SSG vs ISR vs SSR vs PPR
เพื่อให้เห็นภาพชัดขึ้นว่า PPR ต่างจาก rendering model อื่นๆ ยังไง ลองดูตารางเปรียบเทียบนี้:
| คุณสมบัติ | SSG | ISR | SSR | PPR |
|---|---|---|---|---|
| สร้าง HTML เมื่อไหร่ | Build time | Build + background | ทุก request | Build (static) + request (dynamic) |
| ความสดของข้อมูล | เก่าจนกว่า rebuild | อัปเดตเป็นระยะ | สดใหม่เสมอ | ผสมผสานทั้งสองแบบ |
| TTFB (Time to First Byte) | เร็วมาก | เร็วมาก | ช้ากว่า | เร็วมาก |
| รองรับ Personalization | ไม่ได้ | ไม่ได้ | ได้ | ได้ (บางส่วน) |
| SEO | ดีเยี่ยม | ดีเยี่ยม | ดี | ดีเยี่ยม |
| เหมาะกับ | เนื้อหาไม่เปลี่ยน | เนื้อหาเปลี่ยนบ้าง | เนื้อหา real-time | เนื้อหาผสมผสาน |
PPR ได้เปรียบที่สุดเมื่อหน้าเว็บมีส่วน static จำนวนมากผสมกับส่วน dynamic เล็กน้อย เช่น หน้าสินค้าที่มีรายละเอียด static แต่มีตะกร้าและสินค้าแนะนำที่เป็น dynamic ส่วนตัวแล้ว คิดว่านี่คือ use case ของเว็บส่วนใหญ่เลย
ผลลัพธ์ด้าน Performance ที่ได้จาก PPR
มาถึงส่วนที่น่าตื่นเต้น — ตัวเลข performance จริงๆ
จากข้อมูลที่ Vercel และชุมชนนักพัฒนารายงาน PPR สามารถปรับปรุง performance ได้อย่างมีนัยสำคัญ:
- First Contentful Paint (FCP) เร็วขึ้นประมาณ 73% — จาก ~680ms เหลือ ~180ms
- Largest Contentful Paint (LCP) เร็วขึ้นประมาณ 68% — จาก ~890ms เหลือ ~280ms
- Initial page load เร็วขึ้น 60-80% เมื่อเทียบกับหน้าที่เป็น fully dynamic
ตัวเลขเหล่านี้อาจแตกต่างกันไปตามความซับซ้อนของแอปและ infrastructure ที่ใช้นะ แต่แนวโน้มชัดเจนว่า PPR ช่วย Core Web Vitals ได้มาก โดยเฉพาะ FCP ที่ปรับปรุงขึ้นจนเกือบจะเหมือนเว็บ static ล้วนเลย
Best Practices สำหรับการใช้ PPR
1. วาง Suspense Boundary ให้ใกล้ Dynamic Content มากที่สุด
อันนี้สำคัญมาก ยิ่งวาง Suspense boundary แคบเท่าไหร่ static shell ก็จะใหญ่ขึ้นเท่านั้น ซึ่งหมายความว่าผู้ใช้จะเห็นเนื้อหามากขึ้นทันที:
// ❌ แย่ — Suspense ห่อทั้งหน้า ทำให้ static shell น้อยมาก
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<Header />
<StaticContent />
<DynamicWidget />
<Footer />
</Suspense>
)
}
// ✅ ดี — Suspense ห่อเฉพาะส่วน dynamic
export default function Page() {
return (
<>
<Header />
<StaticContent />
<Suspense fallback={<WidgetSkeleton />}>
<DynamicWidget />
</Suspense>
<Footer />
</>
)
}
จากประสบการณ์จริง เห็นหลายคนทำผิดตรงนี้บ่อยมาก อย่าเอา Suspense ไปห่อทั้งหน้าเด็ดขาด
2. ใช้ Parallel Suspense Boundaries
ถ้ามี dynamic content หลายส่วนที่ไม่ขึ้นต่อกัน ให้แยก Suspense boundary แต่ละอัน เพื่อให้ stream เข้ามาแบบ parallel ไม่ต้องรอกัน ส่วนไหนโหลดเสร็จก่อนก็แสดงก่อน
3. ออกแบบ Fallback UI ให้มีคุณภาพ
Fallback UI (skeleton) ที่อยู่ใน Suspense จะถูกรวมเป็นส่วนหนึ่งของ static shell — ดังนั้นมันจะถูก prerender ด้วย ควรออกแบบให้มีขนาดและ layout ใกล้เคียงกับ content จริงมากที่สุด เพื่อลด layout shift (CLS) อย่าใช้แค่ข้อความ "กำลังโหลด..." เฉยๆ
4. ใช้ use cache คู่กับ PPR
ข้อมูลที่ไม่ต้องสดใหม่ทุก request แต่ก็ไม่ต้องเป็น dynamic ด้วย ใช้ 'use cache' กับ cacheLife เพื่อ cache ไว้ได้ ข้อมูลเหล่านี้จะถูกรวมเข้าใน static shell ให้อัตโนมัติ ซึ่งเป็นการ optimize ที่ทำง่ายแต่ได้ผลดีมาก
5. ระวัง Dynamic APIs ที่ซ่อนอยู่
อันนี้เป็นกับดักที่เจอกันบ่อย บางทีคุณอาจเรียก cookies() หรือ headers() โดยไม่รู้ตัวผ่าน library ภายนอก ตรวจสอบว่า component ไหนเรียกใช้ dynamic APIs บ้าง เพราะมันจะทำให้ component นั้น (และ parent ขึ้นไปจนถึง Suspense boundary ที่ใกล้ที่สุด) กลายเป็น dynamic ทั้งหมด
เมื่อไหร่ควรใช้ PPR เมื่อไหร่ไม่ควร
เหมาะกับ PPR:
- หน้า E-Commerce — สินค้า (static) + ตะกร้า/แนะนำ (dynamic)
- หน้า Blog/Docs — เนื้อหา (static) + comments/likes (dynamic)
- Dashboard — layout/sidebar (static) + data widgets (dynamic)
- หน้า Landing Page — เนื้อหาหลัก (static) + personalized CTA (dynamic)
อาจไม่จำเป็น:
- หน้าที่เป็น static ล้วน 100% — ใช้
use cacheที่ระดับ page ธรรมดาก็พอ - หน้าที่เป็น dynamic ล้วน 100% — เช่น real-time chat หรือ live dashboard ที่ทุกส่วนต้อง render ใหม่ทุก request
- SPA ที่ทำ client-side fetching ทั้งหมด — PPR ทำงานบน server-side rendering ถ้าทุกอย่างอยู่ฝั่ง client อยู่แล้วก็ไม่ได้ประโยชน์
Dynamic Routes กับ PPR
หน้าที่มี dynamic route เช่น /products/[id] ก็ใช้ PPR ได้เช่นกัน Next.js จะสร้าง static shell ที่เป็น template แล้วเติมข้อมูลตอน request time ถ้าข้อมูลถูก cache ด้วย use cache ก็จะได้ performance ใกล้เคียง static เลย:
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { getProduct } from '@/lib/data'
interface Props {
params: Promise<{ id: string }>
}
export default async function ProductPage({ params }: Props) {
const { id } = await params
// getProduct ใช้ 'use cache' → ข้อมูลถูก cache
// จึงรวมใน static shell ได้แม้จะเป็น dynamic route
const product = await getProduct(id)
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<Suspense fallback={<div>กำลังโหลดข้อมูลการจัดส่ง...</div>}>
<ShippingInfo productId={id} />
</Suspense>
</main>
)
}
ข้อดีคือผู้ใช้เห็นข้อมูลสินค้าทันทีจาก cache ส่วนข้อมูลที่ต้อง personalized (เช่น ค่าจัดส่งตามที่อยู่ผู้ใช้) จะ stream เข้ามาทีหลัง UX ดีขึ้นมากเมื่อเทียบกับการรอ server render ทั้งหน้า
Revalidation ใน PPR
เนื่องจาก PPR ทำงานร่วมกับ Cache Components คุณจึงใช้กลไก revalidation ทั้งหมดที่มีอยู่ได้:
- Time-based — ใช้
cacheLife('hours')หรือ custom profile เพื่อกำหนดอายุ cache - Tag-based — ใช้
cacheTag()ร่วมกับrevalidateTag()เพื่อ invalidate cache เมื่อข้อมูลเปลี่ยน - On-demand — เรียก
revalidatePath()หรือrevalidateTag()จาก Server Action เมื่อมีการอัปเดตข้อมูล
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
// อัปเดตข้อมูลในฐานข้อมูล
await db.product.update({
where: { id },
data: { name: data.get('name') as string },
})
// Invalidate cache ของสินค้านี้
revalidateTag(`product-${id}`)
}
เมื่อ cache ถูก invalidate static shell จะถูก regenerate ใหม่ในคำขอถัดไป ผู้ใช้ได้ข้อมูลล่าสุดโดยไม่ต้องรอ rebuild ทั้งแอป ระบบ revalidation ตรงนี้ทำงานได้ดีมาก แทบไม่ต้อง config อะไรเพิ่ม
คำถามที่พบบ่อย (FAQ)
PPR กับ Streaming/Suspense ธรรมดาต่างกันยังไง?
ความแตกต่างหลักอยู่ที่ timing ของการ render ส่วน static Streaming/Suspense ปกติจะ render ทั้งหน้าตอน request time ทุกครั้ง แม้ส่วน static ก็ต้อง render ใหม่ แต่ PPR จะ prerender ส่วน static ตอน build time จริงๆ แล้ว serve จาก CDN ได้เลย ทำให้ TTFB เร็วกว่ามาก เพราะ server ไม่ต้องทำงานกับส่วน static ซ้ำอีก
PPR ช่วยเรื่อง SEO ไหม?
ช่วยอย่างมากเลย เพราะ static shell ถูก prerender เป็น HTML ที่สมบูรณ์ search engine crawlers สามารถ index เนื้อหาหลักได้ทันทีโดยไม่ต้องรอ JavaScript ส่วน dynamic content ที่ stream เข้ามาก็เป็น HTML เช่นกัน (ไม่ใช่ client-side fetching) จึง crawlable เช่นเดียวกัน ดีกว่า Client-Side Rendering มากจริงๆ
ต้องใช้ Vercel ถึงจะใช้ PPR ได้หรือเปล่า?
ไม่จำเป็นเลย PPR ทำงานกับ Next.js standalone mode ได้ ดังนั้นสามารถ deploy บน Docker, AWS หรือ VPS อะไรก็ได้ที่รัน Node.js แต่ต้องยอมรับว่า Vercel เป็น platform แรกที่รองรับ PPR อย่างเต็มรูปแบบ รวมถึง Edge Caching ที่ช่วย serve static shell จาก CDN ใกล้ผู้ใช้ ถ้า deploy ที่อื่นก็ยังใช้ PPR ได้แต่อาจต้องตั้งค่า caching layer เอง
สามารถใช้ PPR ร่วมกับ ISR แบบเดิมได้ไหม?
ไม่ได้ เมื่อเปิด cacheComponents: true ระบบ ISR แบบเดิมจะถูกปิดอัตโนมัติ เพราะ Next.js 16 ไม่รองรับการทำงานทั้งสองระบบพร้อมกัน แต่ไม่ต้องกังวล PPR + Cache Components ทำทุกอย่างที่ ISR ทำได้ และมากกว่า มี granularity ในการ cache ที่ละเอียดกว่า และรองรับ revalidation ทั้ง time-based และ tag-based เหมือนเดิม
ถ้าลืมห่อ dynamic component ด้วย Suspense จะเกิดอะไรขึ้น?
Next.js จะแจ้ง error ว่า "Uncached data was accessed outside of Suspense" ตอน development หรือ build time ซึ่งเป็นกลไกป้องกันไม่ให้คุณลืมจัดการ dynamic content โดยไม่ตั้งใจ คุณต้องเลือกอย่างใดอย่างหนึ่ง: ห่อด้วย <Suspense> เพื่อ stream ตอน request time หรือ mark ด้วย 'use cache' เพื่อรวมเข้าใน static shell ไม่มีทางสายกลาง