ข้ามไปยังเนื้อหา

สี่ Caching Layers

caching ของ Next.js คือ สี่ชั้นแยกกัน ได้แก่ Request Memoization, Data Cache, Full Route Cache และ Client Cache (ชื่อปัจจุบันในเอกสารของสิ่งที่เคยเรียกว่า Router Cache) และตั้งแต่ v15 Data Cache เป็นแบบ opt-in เพราะ fetch ไม่ถูก cache โดย default อีกต่อไป

แต่ละชั้น cache คนละอย่าง อยู่คนละที่ และ invalidate คนละแบบ การแยกทั้งสี่ชั้นออกจากกันในหัวคือทักษะทั้งหมดของเรื่องนี้

ชั้นcache อะไรอยู่ที่ไหนอยู่นานแค่ไหนinvalidate ยังไง
Request Memoizationค่าที่ return จาก fetch / cache() ที่เหมือนกันใน render เดียวServer, per request (React)หนึ่ง request / render passจบเองเมื่อ render เสร็จ
Data Cacheผลของ data fetch ใช้ซ้ำข้าม request และ deployServer (persistent)จนกว่าจะ revalidaterevalidateTag(tag, profile), updateTag, revalidatePath หรือ revalidate แบบ time-based
Full Route CacheRSC payload กับ HTML ของ route ที่ render แบบ staticServer (persistent, build หรือ runtime)จนกว่าจะ deploy หรือ data revalidaterevalidate data ข้างใต้ หรือ redeploy
Client Cache (ชื่อเดิม Router Cache)RSC payload ของ route ที่เข้าชมหรือ prefetchClient (browser memory)จนกว่าจะ refresh หน้า; staleTimes คุมความสดrefresh หน้า, router.refresh(), cookies.set / cookies.delete หรือ revalidateTag / revalidatePath / updateTag

สามในสี่ชั้นอยู่บน server และซ้อนกัน Request Memoization dedupe call ที่เหมือนกันภายใน render เดียว Data Cache เก็บผล fetch ข้าม request Full Route Cache เก็บผล render ของ static route เพื่อให้ request ข้ามการ render ไปเลย

จุดที่มือใหม่พลาด ตั้งแต่ Next.js 15 fetch ธรรมดาไม่เขียนอะไรลง Data Cache เลย เรา opt-in ทีละ call

// Not cached — hits the origin on every request (the Next.js 15+ default)
await fetch('https://api.example.com/prices')
// Opt in to the Data Cache indefinitely
await fetch('https://api.example.com/config', { cache: 'force-cache' })
// Opt in with a 60-second lifetime
await fetch('https://api.example.com/feed', { next: { revalidate: 60 } })
// Opt in AND tag for on-demand invalidation — the tag alone opts into nothing
await fetch('https://api.example.com/cart', {
next: { revalidate: 60, tags: ['cart'] },
})

next.tags เดี่ยว ๆ ไม่ทำให้ fetch นั้น opt-in เข้า Data Cache — เอกสารที่ bundle มาของ fetch บอกตรง ๆ ว่า “caching is opt-in” มีแค่ cache: 'force-cache' หรือตัวเลข next.revalidate เท่านั้นที่ทำได้ พิสูจน์แล้วในโพรบ: dynamic route ที่ fetch endpoint นับ request ด้วย next: { tags: [...] } อย่างเดียว (ไม่มี cache ไม่มี revalidate) คืนค่านับที่สดและเพิ่มขึ้นทุกครั้งจากสาม request แยกกัน — ไม่มีอะไรถูก cache เลย ดังนั้น tag จึงไม่มีอะไรให้ invalidate จับคู่ tag กับ cache: 'force-cache' หรือ next.revalidate เสมอเมื่อต้องการให้ revalidateTag ทำงานจริง

ชั้นที่สี่อยู่ใน browser (เอกสารและบทความเก่า ๆ เรียกมันว่า Router Cache) เมื่อเรา navigate หรือ hover <Link> ที่ prefetch ไว้ Next.js เก็บ RSC payload ของ route ไว้ใน memory ทำให้ back และ forward ทันที ตั้งแต่ v15 ค่า default ของ staleTimes.dynamic คือ 0 ดังนั้น dynamic page จะไม่ถูกเก็บฝั่ง client จนกว่าเราจะเพิ่มค่า

// next.config.ts — opt into holding dynamic routes for 30s in the Client Cache (still experimental)
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
staleTimes: { dynamic: 30 },
},
}
export default nextConfig

ใน Next.js 14 fetch default เป็น force-cache tutorial เลยสอนว่า “Next.js cache ทุกอย่างให้อัตโนมัติ” ซึ่ง ผิดสำหรับ v15 และ v16 วันนี้ caching เป็นแบบ layered และ opt-in fetch ธรรมดาไม่เขียนอะไรลง Data Cache และ cache จะว่างจนกว่าเราจะร้องขอ แต่สังเกตว่ามัน ไม่ได้ หมายความว่า route กลายเป็น dynamic — fetch ธรรมดาที่ถูกเรียกก่อน request-time API ใด ๆ จะถูก fetch ครั้งเดียวตอน build และ route ยังเป็น static มีแค่ cache: 'no-store' (หรือ revalidate: 0) เท่านั้นที่ทำให้ route เป็น dynamic GET Route Handler ก็เปลี่ยนแบบเดียวกัน เป็น dynamic จนกว่าเราจะเพิ่ม export const dynamic = 'force-static' (หรือภายใต้ Cache Components ย้ายการเข้าถึง data ไปไว้ใน function 'use cache')

flowchart TD
  A["Browser navigates"] --> B["Client Cache (browser)"]
  B -->|miss| C["Full Route Cache (server)"]
  C -->|miss or dynamic| D["Render Server Components"]
  D --> E["Request Memoization (per render)"]
  E --> F["Data Cache (server)"]
  F -->|miss| G["Origin: API or database"]
request ไหลผ่าน cache ทั้งสี่ชั้นยังไง

request ไหลผ่านสี่ชั้นจากบนลงล่าง เหมือน diagram ข้างบน: Client Cache ก่อน (browser มี route นี้อยู่แล้วหรือเปล่า?) แล้วค่อย Full Route Cache (server render อันนี้ไปแล้วหรือยัง?) แล้วถ้าทั้งคู่ miss render ถึงจะรันจริง — และ ภายใน render นั้น Request Memoization กับ Data Cache ทำงานร่วมกันเพื่อให้การ fetch data ของ render นั้นถูกลง แต่ละชั้นมีไว้เพื่อให้ request ข้ามชั้นถัดไปได้มากที่สุดเท่าที่จะทำได้

สิ่งที่ได้ประโยชน์ต้นทุน
Data Cache เป็น opt-in โดย default (v15+)ไม่มี data ค้างแบบไม่รู้ตัว — ทุก fetch สดเสมอเว้นแต่จะขอเป็นอื่นย้ายจาก code v14 มา หมายความว่าทุก fetch ที่พึ่ง default caching จะกลายเป็น live request แบบเงียบ ๆ
next: { revalidate: N } ต่อ fetchคุมละเอียดต่อ callลืมใส่ใน fetch ตัวใดตัวหนึ่งง่าย ทำให้ความสดไม่สม่ำเสมอในหน้าเดียวกัน
next: { tags: [...] }invalidate on-demand แม่นยำในภายหลังมีประโยชน์ก็ต่อเมื่อมีที่ไหนสักแห่งเรียก revalidateTag ด้วย — tag ที่ไม่มีใคร revalidate คือของหนักที่ไม่มีประโยชน์
Full Route Cacheข้ามการ render ไปเลยสำหรับ static routeช่วยได้แค่ route ที่ เป็น static ได้จริง — request-time API ตัวเดียวใน tree ก็ทำให้ route หลุดจาก static แล้ว
  • “Next.js cache ทุกอย่างให้อัตโนมัติ” — จริงใน Next.js 14 ไม่จริงตั้งแต่ 15 fetch ธรรมดาไม่มี option ไม่เขียนอะไรลง Data Cache และตี origin ทุก request
  • “fetch ธรรมดาที่ไม่ cache ทำให้ route เป็น dynamic” — ไม่จริง fetch ที่ถูกเรียกก่อน request-time API ใด ๆ ยังรันครั้งเดียวตอน build และ route ยังเป็น static มีแค่ cache: 'no-store' (หรือ revalidate: 0) — หรือ request-time API จริง ๆ อย่าง cookies() — เท่านั้นที่ทำให้เป็น dynamic
  • “Client Cache กับ Data Cache คือของเดียวกัน” — เป็นคนละชั้นที่มี lifetime ต่างกัน Data Cache อยู่บน server ข้าม request ส่วน Client Cache อยู่ใน memory ของ browser และถูก invalidate ด้วย router.refresh(), page reload หรือการ revalidate ที่ trigger จาก server
  • “Request Memoization กับ Data Cache คือชั้นเดียวกันแค่คนละชื่อ” — ไม่ใช่ Request Memoization ตายไปพร้อม render pass ส่วน Data Cache คือสิ่งที่เก็บผล fetch ข้าม request และ deploy

เมื่อ snippet ที่ generate มา fetch data และพึ่งพา caching behavior ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:

  • มี comment หรือสมมติฐานว่า “Next.js cache สิ่งนี้ให้อัตโนมัติ” หรือเปล่า? นั่นจริงใน 13/14 และไม่จริงใน 15/16 — ตรวจสอบว่า fetch ถูกส่ง option อะไรจริง ๆ
  • โค้ดสมมติว่า fetch ธรรมดาไม่มี option จะ fetch data สดทุก request หรือเปล่า? บน route ที่ไม่มี request-time API เลย มันตรงข้ามกัน — fetch ครั้งเดียวตอน build แล้วแช่แข็งใน static HTML จนกว่าจะ deploy ครั้งถัดไป
  • next: { tags: [...] } ถูกใช้ โดยไม่มี cache: 'force-cache' หรือตัวเลข next.revalidate คู่กันหรือเปล่า? tag เดี่ยว ๆ ไม่ opt-in อะไรเลย ไม่มี cache entry ให้ revalidateTag invalidate
  • next: { tags: [...] } ถูกใช้ที่ไหนโดยไม่มี revalidateTag เรียกจับคู่ใน codebase หรือเปล่า? tag ที่ไม่มีใคร revalidate จะไม่ถูก invalidate เลย
  • โค้ดสมมติว่า fetch ที่ไม่ cache ทำให้ route เป็น dynamic หรือเปล่า? มันไม่ทำ ด้วยตัวมันเอง — มีแค่ cache: 'no-store', revalidate: 0 หรือ request-time API เท่านั้นที่ทำแบบนั้น

🐛 หา bug ในโค้ดนี้

export default async function PricePage() {
// No cache option — Next.js re-fetches fresh data on every request, right?
const res = await fetch('https://api.example.com/live-price')
const price = await res.json()
return <p>${price.value}</p>
}
เฉลย

ตรงข้ามกับที่คิด — หน้านี้ไม่มี request-time API เลยแม้แต่ตัวเดียว (ไม่มี cookies() ไม่มี headers() ไม่มี dynamic segment) ดังนั้นภายใต้ default “auto no cache” fetch จะรัน ครั้งเดียวตอน next build แล้ว HTML ที่ได้ก็ถูกเสิร์ฟแบบ static ทุก request หลังจากนั้น แช่แข็งจนกว่าจะ deploy ครั้งถัดไป พิสูจน์แล้วในโพรบ: หน้าแบบนี้ที่ชี้ไปยัง endpoint นับ request ฝัง count เดียวลงใน static HTML ตอน build แล้วสาม request แยกกันของ next start คืนตัวเลขแช่แข็งตัวเดิมเป๊ะ ๆ — endpoint ไม่เคยถูกเรียกอีกเลย หน้า “ราคาสด” ต้องการ cache: 'no-store' ชัดเจน (fetch ใหม่ทุก request) หรือ next: { revalidate: 5 } สั้น ๆ (ยอมให้ค้างสองสามวินาที) การไม่ใส่ option เลยคือทางเลือกเดียวที่รับประกันว่ามันจะไม่อัปเดตเลย

ตั้งแต่ version ไหนที่ fetch ไม่ cache โดย default ใน Next.js?
ชั้นไหนในสี่ชั้นที่อยู่ใน browser?
ชั้นไหน dedupe fetch ที่เหมือนกันภายใน render เดียว?
revalidateTag invalidate อะไร?
route นี้ไม่มี cookies(), headers() หรือ dynamic segment เลย ใน Next.js 16 หลัง next build จะเกิดอะไรขึ้นจริง ๆ?
export default async function PricePage() {
  // No cache option — Next.js re-fetches fresh data on every request, right?
  const res = await fetch('https://api.example.com/live-price')
  const price = await res.json()
  return <p>{price.value}</p>
}
บน dynamic route (ที่ใช้ cookies() ที่อื่น) และไม่มี cache หรือ next.revalidate fetch นี้ทำอะไรจริง ๆ ในแต่ละ request?
await fetch('https://api.example.com/feed', { next: { tags: ['feed'] } })