use cache และ Cache Components
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Cache Components พลิกโมเดล แทนที่จะจำ cache option ต่อ fetch เรา mark function, file หรือ component ด้วย directive 'use cache' เพื่อบอกว่า “cache อันนี้” แล้วปรับ lifetime กับ invalidation ด้วย cacheLife และ cacheTag
directive use cache
หัวข้อที่มีชื่อว่า “directive use cache”ใส่ string 'use cache' ไว้บนสุดของ function หรือ file แล้วทุกอย่างที่ function นั้น return จะถูก cache ไม่ว่าจะเป็น fetch, query database หรือทั้ง component ที่ render แล้ว วิธีนี้มาแทนการโปรย cache option ตาม call ทีละอัน
import { cacheLife, cacheTag } from 'next/cache'
async function getData() { 'use cache' cacheLife('hours') cacheTag('data') const res = await fetch('https://api.example.com/data') return res.json()}จาก unstable_cache ไปสู่ use cache
หัวข้อที่มีชื่อว่า “จาก unstable_cache ไปสู่ use cache”โค้ดที่เขียนสำหรับ Next.js 14 (หรือ AI model ที่ train มาจากยุคนั้น) ยังใช้ unstable_cache — ห่อ function, ส่ง key-parts array, ส่ง options object เอกสารที่ bundle มาบอกตรง ๆ ว่า “This API has been replaced by use cache in Next.js 16” มันยังทำงานได้อยู่ (ไม่ได้ถูกลบ) แต่การ mapping นี้ควรจำให้ขึ้นใจ เพราะเจอได้ในการ review โค้ดเก่าแทบทุกครั้ง:
unstable_cache | สิ่งที่เทียบเท่าใน 'use cache' |
|---|---|
unstable_cache(fn, keyParts, options) ห่อ function จากข้างนอก | 'use cache' อยู่ ข้างใน ตัว function เอง เป็นบรรทัดแรก |
keyParts: ['posts'] — ส่วนต่อท้าย cache key ที่ตั้งเอง | ไม่ต้องมี — 'use cache' derive key จาก argument และ closure ของ function เองอัตโนมัติ |
options.tags: ['posts'] | cacheTag('posts') เรียกข้างในตัว function |
options.revalidate: 3600 (วินาที) | cacheLife({ revalidate: 3600 }) หรือ named profile อย่าง cacheLife('hours') |
ทำงานได้แม้ cacheComponents ปิดอยู่ | ต้องการ cacheComponents: true |
| สถานะใน docs ของ Next.js 16 | Deprecated แล้ว แต่ยัง import ได้ — ไม่ได้ถูกลบ |
import { unstable_cache } from 'next/cache'import { db } from '@/lib/db'
// Old model: keyParts is a manual cache-key suffix; tags/revalidate live in// the options object.export const getPosts = unstable_cache( async () => db.post.findMany(), ['posts'], { tags: ['posts'], revalidate: 3600 },)import { cacheLife, cacheTag } from 'next/cache'import { db } from '@/lib/db'
// New model: no manual key — 'use cache' derives it from the function's// arguments and closure. Tag and lifetime are set inline, in scope.export async function getPosts() { 'use cache' cacheTag('posts') cacheLife({ revalidate: 3600 }) return db.post.findMany()}export const db = { post: { findMany: async () => [] as { id: string; title: string }[], },}cacheLife และ cacheTag
หัวข้อที่มีชื่อว่า “cacheLife และ cacheTag”cacheLife ตั้งว่า entry จะสดอยู่นานแค่ไหน เป็น named profile อย่าง 'hours' หรือ object ที่ระบุชัด Next.js มี named profile ให้หกแบบนอกจาก default แต่ละแบบสมดุลสามค่าเวลาเดียวกัน (stale, revalidate, expire):
| Profile | Use case | stale | revalidate | expire |
|---|---|---|---|---|
default | เนื้อหาทั่วไป | 5 นาที | 15 นาที | ไม่มี |
seconds | data แบบ real-time (ราคาหุ้น, สกอร์สด) | 30 วิ | 1 วิ | 1 นาที |
minutes | อัปเดตบ่อย (social feed, ข่าว) | 5 นาที | 1 นาที | 1 ชั่วโมง |
hours | อัปเดตหลายครั้งต่อวัน (สต๊อกสินค้า, สภาพอากาศ) | 5 นาที | 1 ชั่วโมง | 1 วัน |
days | อัปเดตรายวัน (blog post, บทความ) | 5 นาที | 1 วัน | 1 สัปดาห์ |
weeks | อัปเดตรายสัปดาห์ (podcast, newsletter) | 5 นาที | 1 สัปดาห์ | 30 วัน |
max | แทบไม่เปลี่ยนเลย (หน้ากฎหมาย, เนื้อหา archive) | 5 นาที | 30 วัน | 1 ปี |
คุณนิยาม named profile ของตัวเองใน next.config.ts ได้ด้วย ควบคู่หรือแทนที่ชื่อ built-in — ดู Custom cache profiles ด้านล่างสำหรับตัวอย่างเต็มที่ build และเช็กในโพรบแล้ว
cacheTag ติด tag เพื่อ invalidate entry แบบ on-demand ได้ และหนึ่ง call ติดได้มากกว่าหนึ่ง tag: cacheTag('products', 'product-42') ติด tag ให้ entry เดียวกันทั้งแบบกว้าง (สินค้าทุกชิ้น) และแบบเจาะจง (สินค้าชิ้นนี้) ทำให้ mutation invalidate ได้ในระดับความละเอียดที่เหมาะสม revalidateTag(tag, 'max') mark tag ว่า stale แล้วเสิร์ฟ entry เดิมระหว่าง refresh (ใน Next.js 16 argument ตัวที่สองจำเป็น แบบ argument เดียวถูก deprecate และเป็น TypeScript error) ส่วนใน Server Action updateTag(tag) จะ expire ทันทีเพื่อให้ผู้ใช้เห็นสิ่งที่ตัวเองเพิ่งเขียน
import { cacheLife, cacheTag } from 'next/cache'
async function getProduct(id: string) { 'use cache' cacheTag('products', `product-${id}`) cacheLife({ stale: 60, revalidate: 300 }) return db.product.findUnique({ where: { id } })}เปิดใช้: cacheComponents และ Partial Prerendering
หัวข้อที่มีชื่อว่า “เปิดใช้: cacheComponents และ Partial Prerendering”'use cache' ต้องใช้ flag cacheComponents ใน Next.js 16 flag ตัวเดียวกันนี้ ยัง เป็นวิธีเปิด Partial Prerendering ด้วย flag ppr แบบ experimental เดิมและ export experimental_ppr ถูกถอดออกไปแล้ว เมื่อเปิด flag นี้ Next.js จะ prerender shell แบบ static และ stream ส่วน dynamic ที่ไม่ cache เข้ามาตอน request
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true,}
export default nextConfigCustom cache profiles
หัวข้อที่มีชื่อว่า “Custom cache profiles”ขยาย config file เดิมด้วย named cacheLife profile ของตัวเอง ควบคู่กับ flag:
// next.config.ts — extends the minimal config above with a custom profileimport type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true, cacheLife: { biweekly: { stale: 60 * 60 * 24 * 14, // 14 days revalidate: 60 * 60 * 24, // 1 day expire: 60 * 60 * 24 * 14, // 14 days }, },}
export default nextConfig// app/catalog/page.tsx — only type-checks against the config above, not the harness's shared probe configimport { cacheLife } from 'next/cache'
async function getData() { 'use cache' cacheLife('biweekly') return { now: Date.now() }}
export default async function CatalogPage() { const data = await getData() return <p>{data.now}</p>}type overload ของ cacheLife สำหรับชื่อ profile ที่ตั้งเองถูก generate จาก next.config.ts ตัวไหนก็ตามที่อยู่จริงที่ root ของโพรบตอนรัน next dev/next build/next typegen — fence ในบทเรียนที่ชื่อ next.config.ts ไม่ได้เปลี่ยนอันนั้นให้ shared type-check harness เลย นี่คือเหตุผลที่คู่นี้ถูกพิสูจน์ด้วย next build จริงแบบแยกโพรบ (ด้านล่าง) แทนที่จะปล่อยให้ harness เก็บอัตโนมัติ
Build และเช็กในโพรบแล้ว build output ยืนยันว่าตัวเลขของ custom profile ผ่านเข้าไปจริง:
Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found└ ○ /catalog 1d 2wprivate และ remote cache
หัวข้อที่มีชื่อว่า “private และ remote cache”มีสอง variant ที่ต่อยอดโมเดลนี้ ทั้งคู่มาพร้อม Cache Components ใน Next.js 16 'use cache: private' อ่าน cookies(), headers() และ searchParams ได้ตรง ๆ ผลลัพธ์ไม่ถูกเขียนลง shared cache ฝั่ง server เลย — มันถูกเก็บ ต่อ browser ใน memory ของ client ตลอดช่วง stale จึงไม่แชร์ข้ามผู้เข้าชม ส่วน 'use cache: remote' เก็บ entry ใน cache handler ที่แชร์และคงทน (Redis, KV หรืออะไรก็ตามที่ platform ให้ผ่าน cacheHandlers) ทำให้ทุก server instance และทุก user เจอ entry เดียวกัน แลกกับ network round trip
import { cacheLife } from 'next/cache'import { cookies } from 'next/headers'
async function getRecommendations() { 'use cache: private' cacheLife({ stale: 60 }) const sessionId = (await cookies()).get('session-id')?.value return db.recommendations.forSession(sessionId)}flowchart TD A["cacheComponents: true"] --> B["'use cache' function or component"] A --> C["Partial Prerendering"] B --> D["Cached static shell"] C --> D D --> E["Dynamic uncached holes stream in"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”'use cache' ย้ายจุดตัดสินใจเรื่อง caching จาก ตำแหน่งที่เรียก (options object บน fetch แต่ละครั้ง) ไปที่ ตำแหน่งที่นิยาม (directive บนสุดของ function) พอเข้ามาอยู่ใน scope ของ 'use cache' แล้ว cacheLife กับ cacheTag คือ configuration ท้องถิ่นสำหรับหน่วย cache นั้นเดียว — มันไม่มีทางรั่วออกไป และไม่มีอะไรเกี่ยวกับวิธีที่มันถูกเรียกจากข้างนอกที่ต้องเปลี่ยน นั่นคือการพลิกโมเดลทั้งหมดจากแบบเดิม — caching กลายเป็นคุณสมบัติของ function ไม่ใช่ของแต่ละ call
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
'use cache' บน function | ไม่ต้องมี cache key เอง tag/lifetime อยู่ติดกับ logic ที่มันอธิบาย | ต้องการ cacheComponents: true — flag แบบ all-or-nothing สำหรับ route segment config ที่มันมาแทน |
Built-in cacheLife profile | ชื่อสื่อความหมาย ('days' อ่านแล้วรู้ว่า “อัปเดตรายวัน”) | ตัวเลขจริงตายตัวเว้นแต่จะ override — อ่านตารางถ้าความแม่นยำสำคัญ |
Custom cacheLife profile ใน next.config.ts | คุมได้ชัดเจน ใช้ชื่อซ้ำได้ทั่วแอป | มีอีกจุดที่ต้องดูแลให้ตรงกัน — ชื่อมีความหมายแค่ตามที่ config บอกไว้ตอนนั้น |
cacheTag หลายค่าบน entry เดียว | invalidate แบบกว้าง ('products') หรือแบบแคบ ('product-42') จาก mutation คนละตัว | ทุก tag ที่เพิ่มมาคือสิ่งที่ต้อง revalidate ให้ถูกต้อง ไม่งั้นจะดูเหมือน invalidate แล้วทั้งที่ยังไม่ได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
unstable_cacheถูกลบใน Next.js 16 แล้ว” — ไม่จริง มันยัง compile และรันได้อยู่ แค่เอกสาร mark ว่าถูกแทนด้วย'use cache'และแนะนำให้ migrate - “
'use cache'ต้องมี cache key เองเหมือนkeyPartsของunstable_cache” — ไม่จริง directive derive key จาก argument และ closure ของ function เองอัตโนมัติ - “เพิ่ม
cacheTagตัวที่สองจะแทนที่ตัวแรก” — ไม่จริงcacheTag('products', 'product-42')ติดทั้งสอง tag ให้ entry เดียวกัน ตัวไหนก็ invalidate มันได้ทีหลัง - “
'use cache: private'เป็นแค่ cache ธรรมดาที่อายุสั้นกว่า” — ไม่ใช่ มันเป็นที่เก็บคนละที่เลย ไม่เคยเขียนลง shared cache ฝั่ง server เก็บต่อ browser แทน โดยเฉพาะเพื่อให้อ่านcookies()/headers()ข้างในได้อย่างปลอดภัย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ snippet ที่ generate มาใช้ unstable_cache หรือ 'use cache' ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:
unstable_cacheถูกใช้โดยไม่มีtagsใน options object หรือเปล่า? ถ้าไม่มี tag จะไม่มีทางให้revalidateTagเล็ง entry นั้นได้เลย — มันจะ expire ได้แค่ตาม timer ของrevalidate(หรือตอน redeploy ทั้งแอป) ซึ่งทำลาย on-demand invalidation แบบเงียบ ๆ- function
'use cache'เรียกcacheLifeหรือเปล่า? การไม่ใส่ไม่ error — มันแค่ fallback ไป profiledefault(revalidate 15 นาที) แบบเงียบ ๆ ซึ่งเข้าใจผิดเป็น “ไม่ cache” ได้ง่าย cacheTagถูกเรียกด้วย tag เดียวทั้งที่ entry ต้องการสอง tag ตาม logic (tag ระดับ collection กับ tag ระดับ item) หรือเปล่า?cacheTagรับหลายค่าในหนึ่ง call ได้ ไม่ต้องเรียกสองครั้งหรือเลือกแค่ความละเอียดเดียว- โค้ดสมมติว่า
'use cache'ทำงานได้โดยไม่มีcacheComponents: trueในnext.config.tsหรือเปล่า? ไม่ได้ directive ต้องการ flag นี้ และ export แบบ route-segment-config เดิม (dynamic,revalidate,fetchCache) จะทำให้ build ล้มทันทีที่เปิด flag นี้
🐛 หา bug ในโค้ดนี้
import { unstable_cache } from 'next/cache'import { db } from '@/lib/db'
export const getPosts = unstable_cache(async () => {return db.post.findMany()})เฉลย
ไม่มี tags ถูกส่งใน options object (ที่ถูกละไว้) และไม่มี keyParts array ด้วย เอกสารที่ bundle มาอธิบายว่า tags คือสิ่งที่ revalidateTag ใช้เล็ง entry — ถ้าไม่มี cached result นี้จะ stale ได้แค่ตาม timer ของตัวเอง (ในที่นี้ไม่มี revalidate เลยด้วย เลย cache ไปตลอด) หรือตอนแอป redeploy Server Action ที่เรียก revalidateTag('posts') หลังสร้าง post ใหม่ทีหลังจะไม่ทำอะไรกับ entry นี้เลย เพราะมันไม่เคยถูก tag ว่า 'posts' — bug ที่ไม่มี error ไม่มี warning มีแต่ data ค้างตลอดไป ทางแก้คือ unstable_cache(async () => db.post.findMany(), ['posts'], { tags: ['posts'] }) หรือดีกว่านั้นคือ migrate ไปเป็น 'use cache' พร้อม cacheTag('posts') ชัดเจน