สี่ 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 และ deploy | Server (persistent) | จนกว่าจะ revalidate | revalidateTag(tag, profile), updateTag, revalidatePath หรือ revalidate แบบ time-based |
| Full Route Cache | RSC payload กับ HTML ของ route ที่ render แบบ static | Server (persistent, build หรือ runtime) | จนกว่าจะ deploy หรือ data revalidate | revalidate data ข้างใต้ หรือ redeploy |
| Client Cache (ชื่อเดิม Router Cache) | RSC payload ของ route ที่เข้าชมหรือ prefetch | Client (browser memory) | จนกว่าจะ refresh หน้า; staleTimes คุมความสด | refresh หน้า, router.refresh(), cookies.set / cookies.delete หรือ revalidateTag / revalidatePath / updateTag |
ฝั่ง server: memoization, data และ route cache
หัวข้อที่มีชื่อว่า “ฝั่ง server: memoization, data และ route cache”สามในสี่ชั้นอยู่บน 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 indefinitelyawait fetch('https://api.example.com/config', { cache: 'force-cache' })
// Opt in with a 60-second lifetimeawait fetch('https://api.example.com/feed', { next: { revalidate: 60 } })
// Opt in AND tag for on-demand invalidation — the tag alone opts into nothingawait 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 ทำงานจริง
ฝั่ง client: Client Cache
หัวข้อที่มีชื่อว่า “ฝั่ง client: Client Cache”ชั้นที่สี่อยู่ใน 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ทำไม “fetch ไม่ cache โดย default” ถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไม “fetch ไม่ cache โดย default” ถึงสำคัญ”ใน 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"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ 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 ให้revalidateTaginvalidatenext: { 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 เลยคือทางเลือกเดียวที่รับประกันว่ามันจะไม่อัปเดตเลย