Next.js 16 use cache คืออะไร? คู่มือ Cache Components ฉบับสมบูรณ์

เรียนรู้ Cache Components ใน Next.js 16 ตั้งแต่พื้นฐาน — ใช้ use cache, cacheLife, cacheTag เพื่อ cache ในระดับ function และ component พร้อม migration guide จาก unstable_cache และตัวอย่างตั้งค่า Redis cache handler สำหรับ production

ทำไม Cache Components ถึงเปลี่ยนเกมการ Caching ใน Next.js

ถ้าคุณเคยใช้ Next.js App Router มาสักระยะ น่าจะเคยรู้สึกปวดหัวกับระบบ caching ใน Next.js 14-15 กันบ้าง ระบบเดิมมันทำงานแบบ implicit — คือ framework จะ cache ให้อัตโนมัติโดยที่คุณอาจไม่รู้ตัวด้วยซ้ำ ข้อมูลที่ fetch มาอาจถูก cache ไว้โดยที่คุณไม่ได้ขอ แล้วพอจะ invalidate ก็ทำได้ลำบากอีก

พูดตรงๆ เลย มันค่อนข้างน่าหงุดหงิด

แต่ใน Next.js 16 ทีม Vercel ได้ปฏิวัติระบบ caching ทั้งหมดด้วยแนวคิดที่เรียกว่า Cache Components ซึ่งใช้ 'use cache' directive ทำให้การ caching กลายเป็นแบบ explicit — คุณต้องบอกตรงๆ ว่าอยากจะ cache อะไร ไม่มีการเดาอีกต่อไป

สิ่งที่เปลี่ยนไปหลักๆ คือ:

  • Default behavior เปลี่ยน — ทุกอย่างจะถูก render แบบ dynamic ที่ request time เว้นแต่คุณจะบอกให้ cache
  • Granularity ดีขึ้นมาก — cache ได้ตั้งแต่ระดับ function, component ไปจนถึงทั้ง route เลย
  • Cache key สร้างอัตโนมัติ — ไม่ต้องมานั่งกำหนด key เองอีกต่อไป arguments กับ closures จะถูกใช้เป็น key ให้โดยอัตโนมัติ
  • รองรับข้อมูลที่ซับซ้อน — cache ได้ทั้ง JSON, JSX และ output ของ React Server Components

เอาล่ะ มาดูกันว่าต้องตั้งค่าและใช้งานยังไงตั้งแต่ต้นจนจบ

1. เปิดใช้งาน Cache Components ใน Next.js 16

ก่อนอื่นเลย Cache Components เป็นฟีเจอร์แบบ opt-in คุณต้องเปิดใช้งานใน next.config.ts ก่อน:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

เมื่อเปิดแล้ว คุณก็ใช้ 'use cache' directive ได้ทุกที่ในแอปเลย แต่มีข้อควรทราบที่สำคัญมาก:

  • เมื่อเปิด cacheComponents ระบบ ISR (Incremental Static Regeneration) แบบเดิมจะถูกปิดอัตโนมัติ เพราะ Next.js 16 ไม่รองรับการทำงานทั้งสองระบบพร้อมกัน
  • ถ้าแอปของคุณยังพึ่งพา ISR อยู่ ก็ไม่ต้องเปิด cacheComponents ก็ได้ จะใช้ฟีเจอร์อื่นๆ ของ Next.js 16 ได้ตามปกติ

2. วิธีใช้ use cache — สามระดับที่ต้องรู้

คุณสามารถใช้ 'use cache' ได้ 3 ระดับ ขึ้นอยู่กับว่าต้องการ cache ในขอบเขตแค่ไหน:

2.1 ระดับไฟล์ (File-level)

ใส่ 'use cache' ไว้บรรทัดแรกสุดของไฟล์ แค่นี้ทุก export ในไฟล์นั้นก็จะถูก cache ทั้งหมด:

'use cache'

import { cacheLife } from 'next/cache'

// ทุก export จะถูก cache
export default async function ProductPage({ params }: { params: { id: string } }) {
  cacheLife('hours')
  const product = await db.product.findUnique({ where: { id: params.id } })

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  )
}

ข้อสำคัญ: เมื่อใช้ระดับไฟล์ ทุก function ที่ export ต้องเป็น async functions นะ ตรงนี้ลืมไม่ได้เลย

2.2 ระดับ Component (Component-level)

วิธีนี้ใส่ 'use cache' ที่บรรทัดแรกภายใน component function เพื่อ cache เฉพาะ component นั้น ส่วนตัวแล้วผมว่าวิธีนี้ใช้บ่อยที่สุด:

// components/ProductReviews.tsx
import { cacheLife, cacheTag } from 'next/cache'

export async function ProductReviews({ productId }: { productId: string }) {
  'use cache'
  cacheLife('hours')
  cacheTag(`reviews-${productId}`)

  const reviews = await db.review.findMany({
    where: { productId },
    orderBy: { createdAt: 'desc' },
    take: 10,
  })

  return (
    <ul>
      {reviews.map((review) => (
        <li key={review.id}>
          <strong>{review.author}</strong>: {review.comment}
        </li>
      ))}
    </ul>
  )
}

เหมาะมากเวลาที่หน้าเว็บผสมผสานระหว่าง static content กับ dynamic content — cache เฉพาะส่วนที่ต้องการได้เลย

2.3 ระดับ Function (Function-level)

ใช้ภายใน function ธรรมดาที่ไม่ใช่ component ก็ได้ เช่น data-fetching functions ที่เราเขียนแยกไว้ใน lib:

// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getPopularProducts() {
  'use cache'
  cacheLife('days')
  cacheTag('popular-products')

  const products = await db.product.findMany({
    orderBy: { salesCount: 'desc' },
    take: 20,
  })

  return products
}

export async function getProductById(id: string) {
  'use cache'
  cacheLife('hours')
  cacheTag(`product-${id}`)

  return db.product.findUnique({ where: { id } })
}

สังเกตไหมครับว่า arguments ของ function (เช่น id) จะกลายเป็นส่วนหนึ่งของ cache key โดยอัตโนมัติ การเรียก getProductById('abc') กับ getProductById('xyz') ก็จะได้ cache entry คนละตัว ซึ่งสะดวกมากเพราะไม่ต้องมานั่งจัดการ key เอง

3. cacheLife — ควบคุมอายุของ Cache

ฟังก์ชัน cacheLife ใช้คู่กับ 'use cache' เพื่อกำหนดว่าจะ cache ข้อมูลนานแค่ไหน มี 3 ค่าที่ควบคุมพฤติกรรม:

  • stale — ระยะเวลาที่ client ใช้ข้อมูลจาก cache ได้เลยโดยไม่ต้องตรวจสอบกับ server
  • revalidate — หลังจากเวลานี้ คำขอถัดไปจะ trigger การ refresh ข้อมูลใน background
  • expire — หลังจากเวลานี้โดยไม่มีคำขอ ข้อมูลจะหมดอายุเลย คำขอถัดไปต้องรอข้อมูลใหม่

Built-in Profiles

Next.js เตรียม cache profiles สำเร็จรูปมาให้ใช้งานเลย:

import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife('minutes')  // cache สั้นๆ ไม่กี่นาที
  // cacheLife('hours')   // cache ไม่กี่ชั่วโมง
  // cacheLife('days')    // cache หลายวัน
  // cacheLife('weeks')   // cache หลายสัปดาห์
  return fetch('/api/data')
}

Custom Profiles

ถ้า built-in profiles ไม่ตอบโจทย์ คุณก็สร้าง profile เองได้ใน next.config.ts:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    // profile สำหรับข้อมูลสินค้า
    product: {
      stale: 60 * 60,        // 1 ชั่วโมง — client ใช้ cache ได้
      revalidate: 60 * 30,   // 30 นาที — refresh ใน background
      expire: 60 * 60 * 24,  // 24 ชั่วโมง — หมดอายุถ้าไม่มีคำขอ
    },
    // profile สำหรับข้อมูลที่อัปเดตบ่อย
    realtime: {
      stale: 5,
      revalidate: 1,
      expire: 60,
    },
  },
}

export default nextConfig

จากนั้นก็เรียกใช้ชื่อ profile ในโค้ดได้เลย:

import { cacheLife } from 'next/cache'

export async function getProduct(id: string) {
  'use cache'
  cacheLife('product')  // ใช้ custom profile ที่กำหนดไว้
  return db.product.findUnique({ where: { id } })
}

Conditional cacheLife

อันนี้เป็นรูปแบบที่ทรงพลังมากครับ — ใช้ cacheLife แบบมีเงื่อนไข ปรับ cache duration ตามสถานะของข้อมูลจริงได้:

import { cacheLife, cacheTag } from 'next/cache'

async function getPost(slug: string) {
  'use cache'
  cacheTag(`post-${slug}`)

  const post = await db.post.findUnique({ where: { slug } })

  if (!post) {
    // ไม่เจอบทความ → cache สั้นๆ เพื่อลด load ที่ database
    cacheLife('minutes')
    return null
  }

  if (post.status === 'published') {
    // บทความที่เผยแพร่แล้ว → cache นานขึ้น
    cacheLife('days')
  } else {
    // แบบร่าง → cache สั้นๆ
    cacheLife('minutes')
  }

  return post
}

แบบนี้ทำให้ปรับ cache duration ได้ตามข้อมูลจริง ซึ่งเป็นสิ่งที่ ISR แบบเดิมทำไม่ได้เลยนะ ถ้าลองคิดดูดีๆ มันเปิดโอกาสให้ optimize caching ได้ละเอียดกว่าเดิมมากทีเดียว

4. cacheTag และ revalidateTag — Invalidation แบบแม่นยำ

การ invalidate cache เป็นหัวใจสำคัญของระบบ caching ที่ดี ถ้า cache ได้แต่ invalidate ไม่เป็นก็ปวดหัวเอาได้ ใน Cache Components คุณมี 2 เครื่องมือหลัก:

4.1 cacheTag — ติดแท็กให้ Cache Entry

ใช้ cacheTag ภายใน scope ที่มี 'use cache' เพื่อแท็ก cache entry:

import { cacheTag, cacheLife } from 'next/cache'

export async function getUserOrders(userId: string) {
  'use cache'
  cacheLife('hours')
  cacheTag('orders', `user-${userId}-orders`)

  return db.order.findMany({
    where: { userId },
    orderBy: { createdAt: 'desc' },
  })
}

สังเกตว่าใส่ tag ได้หลายตัวพร้อมกัน ทำให้ invalidate ได้หลายมุม — จะ invalidate orders ทั้งหมดก็ได้ หรือเฉพาะ orders ของ user คนเดียวก็ได้ ยืดหยุ่นดี

4.2 revalidateTag — Invalidate จาก Server Action หรือ Route Handler

// app/actions/orders.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function createOrder(formData: FormData) {
  const userId = formData.get('userId') as string

  // สร้าง order ใหม่ใน database
  await db.order.create({
    data: {
      userId,
      items: JSON.parse(formData.get('items') as string),
    },
  })

  // invalidate cache ที่เกี่ยวข้อง
  revalidateTag(`user-${userId}-orders`, 'max')
  revalidateTag('orders', 'max')
}

parameter 'max' ใน revalidateTag ใช้กลยุทธ์ stale-while-revalidate ซึ่งหมายความว่า:

  • ข้อมูลจะถูก mark เป็น stale ทันที
  • แต่ข้อมูลใหม่จะถูก fetch เมื่อมีคนเข้าหน้าที่ใช้ tag นั้นครั้งถัดไป
  • ไม่ทำให้เกิด revalidation พร้อมกันจำนวนมาก (ซึ่งช่วยได้มากตอนมี traffic สูง)

4.3 updateTag — Invalidation แบบทันทีสำหรับ Server Actions

updateTag ถูกออกแบบมาโดยเฉพาะสำหรับ Server Actions เพื่อ expire cache ทันที ไม่ใช่แบบ stale-while-revalidate เหมาะสำหรับ read-your-own-writes scenarios ที่ user ต้องเห็นผลลัพธ์ทันทีหลังกด submit:

'use server'

import { updateTag } from 'next/cache'

export async function updateProfile(formData: FormData) {
  const userId = formData.get('userId') as string
  const name = formData.get('name') as string

  // อัปเดตข้อมูลใน database
  await db.user.update({
    where: { id: userId },
    data: { name },
  })

  // expire cache ทันที — ผู้ใช้จะเห็นข้อมูลใหม่เลย
  updateTag(`user-${userId}`)
}

ความแตกต่างหลักๆ ระหว่าง revalidateTag กับ updateTag:

คุณสมบัติ revalidateTag updateTag
ใช้ได้ที่ไหน Server Actions, Route Handlers Server Actions เท่านั้น
วิธีการ invalidate Stale-while-revalidate Expire ทันที
เหมาะกับ ข้อมูลทั่วไป, batch updates Read-your-own-writes

5. สาม Directive ที่ต้องเลือกให้ถูก

Next.js 16 มี cache directive ให้เลือกใช้ 3 ตัว แต่ละตัวเหมาะกับสถานการณ์ที่ต่างกัน ตรงนี้ต้องเลือกให้ดีเพราะมีผลกับ performance และ architecture ค่อนข้างมาก

5.1 use cache (Default — In-Memory)

เก็บ cache ไว้ใน memory ของ server เหมาะสำหรับแอปที่ deploy บน server เดียว หรือใช้เป็น cache layer แรก:

async function getDashboardStats() {
  'use cache'
  cacheLife('minutes')

  const [users, orders, revenue] = await Promise.all([
    db.user.count(),
    db.order.count({ where: { createdAt: { gte: startOfMonth() } } }),
    db.order.aggregate({ _sum: { total: true } }),
  ])

  return { users, orders, revenue: revenue._sum.total }
}

5.2 use cache: remote (Shared Remote Cache)

เก็บ cache ไว้ใน remote storage อย่าง Redis หรือ KV database เหมาะสำหรับแอปที่ deploy หลาย instances:

async function getGlobalConfig() {
  'use cache: remote'
  cacheLife('days')
  cacheTag('global-config')

  return db.config.findMany()
}

ข้อดีของ use cache: remote:

  • Cache ถูกแชร์ระหว่างทุก server instance
  • ข้อมูลคงอยู่แม้ server restart
  • ลด load ที่ data source ได้อย่างมีประสิทธิภาพ

แต่ก็มีข้อเสียที่ต้องยอมรับ:

  • มี network latency เวลาอ่าน/เขียน cache
  • ต้องตั้งค่า infrastructure เพิ่ม (เช่น Redis)
  • มีค่าใช้จ่ายเรื่อง storage ที่ต้องคิดเผื่อไว้

5.3 use cache: private (Browser-Only, ทดลอง)

Cache เฉพาะใน browser ของผู้ใช้แต่ละคน ไม่เก็บบน server เลย:

import { cookies } from 'next/headers'

async function getUserPreferences() {
  'use cache: private'
  cacheLife('hours')

  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')?.value || 'light'
  const locale = cookieStore.get('locale')?.value || 'th'

  return { theme, locale }
}

จุดเด่นสำคัญ: use cache: private เป็น directive เดียวที่เข้าถึง runtime APIs อย่าง cookies() กับ headers() ได้โดยตรง ซึ่งปกติ use cache ธรรมดาทำไม่ได้

ข้อจำกัดที่ต้องรู้:

  • ยังเป็น experimental feature อยู่
  • กำหนด custom cache handler ไม่ได้
  • Cache จะหายเมื่อ reload หน้าเว็บ

6. ย้ายจาก unstable_cache มาใช้ use cache

ถ้าคุณเคยใช้ unstable_cache ใน Next.js 14-15 อยู่ ถึงเวลาต้อง migrate แล้วครับ เพราะ unstable_cache ถูก deprecated ใน Next.js 16 เรียบร้อยแล้ว

ก่อน (unstable_cache)

import { unstable_cache } from 'next/cache'

const getCachedProducts = unstable_cache(
  async (categoryId: string) => {
    return db.product.findMany({
      where: { categoryId },
      orderBy: { createdAt: 'desc' },
    })
  },
  ['products-by-category'],   // ต้องกำหนด cache key เอง
  {
    tags: ['products'],        // กำหนด tags แยก
    revalidate: 3600,          // กำหนด revalidate แยก
  }
)

// ใช้งาน
const products = await getCachedProducts('electronics')

หลัง (use cache)

import { cacheLife, cacheTag } from 'next/cache'

async function getCachedProducts(categoryId: string) {
  'use cache'
  cacheTag('products', `category-${categoryId}`)
  cacheLife('hours')

  return db.product.findMany({
    where: { categoryId },
    orderBy: { createdAt: 'desc' },
  })
}

// ใช้งานเหมือนเดิม
const products = await getCachedProducts('electronics')

สิ่งที่ดีขึ้นอย่างชัดเจน:

  • ไม่ต้องกำหนด cache key เอง — categoryId จะเป็นส่วนหนึ่งของ key ให้อัตโนมัติ
  • Tags กับ lifetime อยู่ภายใน function เดียวกัน อ่านง่ายกว่าเดิมเยอะ
  • รองรับ return type ที่ซับซ้อนกว่า JSON (รวมถึง JSX ด้วย)
  • Syntax ชัดเจน ดูปุ๊บเข้าใจปั๊บ

7. ตั้งค่า Custom Cache Handler ด้วย Redis

สำหรับ production ที่ deploy หลาย instances การใช้ in-memory cache อย่างเดียวอาจไม่พอ โดยเฉพาะถ้ามี auto-scaling ที่เพิ่ม-ลด server ตลอดเวลา คุณสามารถตั้งค่า Redis เป็น cache backend ได้แบบนี้:

ตั้งค่า cacheHandlers ใน next.config.ts

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandlers: {
    default: './cache-handlers/redis-handler.ts',  // สำหรับ 'use cache'
    remote: './cache-handlers/redis-handler.ts',   // สำหรับ 'use cache: remote'
  },
}

export default nextConfig

สร้าง Redis Cache Handler

ส่วนนี้ต้องใส่ใจหน่อยนะครับ เพราะ interface ที่ Next.js คาดหวังนั้นมี method หลายตัว:

// cache-handlers/redis-handler.ts
import { createClient } from 'redis'

const redis = createClient({ url: process.env.REDIS_URL })
redis.connect()

export default {
  async get(cacheKey: string) {
    const data = await redis.get(cacheKey)
    if (!data) return undefined

    const parsed = JSON.parse(data)
    return {
      value: Buffer.from(parsed.value, 'base64'),
      tags: parsed.tags,
      timestamp: parsed.timestamp,
      expire: parsed.expire,
      revalidate: parsed.revalidate,
    }
  },

  async set(cacheKey: string, pendingEntry: Promise<any>) {
    // สำคัญ: ต้อง await pendingEntry ก่อน
    const entry = await pendingEntry

    const serialized = JSON.stringify({
      value: Buffer.from(entry.value).toString('base64'),
      tags: entry.tags,
      timestamp: entry.timestamp,
      expire: entry.expire,
      revalidate: entry.revalidate,
    })

    const ttl = entry.expire
      ? Math.max(1, Math.floor((entry.expire - Date.now()) / 1000))
      : 60 * 60 * 24 // default 24 ชั่วโมง

    await redis.set(cacheKey, serialized, { EX: ttl })
  },

  async refreshTags(tags: string[]) {
    // คืนค่า timestamp ล่าสุดของแต่ละ tag
    const result: Record<string, number> = {}
    for (const tag of tags) {
      const ts = await redis.get(`tag:${tag}`)
      if (ts) result[tag] = parseInt(ts, 10)
    }
    return result
  },

  async updateTags(tags: string[]) {
    const now = Date.now()
    for (const tag of tags) {
      await redis.set(`tag:${tag}`, now.toString())
    }
  },
}

จุดที่พลาดกันบ่อย: method set จะได้รับ pendingEntry เป็น Promise ที่อาจยังไม่ resolve คุณต้อง await ก่อนจึงจะนำข้อมูลไป store ได้ ถ้าลืม await ตรงนี้จะได้ข้อมูลแปลกๆ เก็บเข้า Redis

8. Cache Components vs ISR — เปรียบเทียบแบบชัดเจน

หลายคนอาจสงสัยว่า Cache Components ต่างจาก ISR ตรงไหน ตารางนี้น่าจะช่วยให้เห็นภาพชัดขึ้น:

คุณสมบัติ Cache Components (use cache) ISR
ความละเอียดในการ cache Function / Component / Route Page / Route เท่านั้น
การควบคุม Explicit (ต้องระบุเอง) Implicit / Time-based
Invalidation revalidateTag, updateTag revalidatePath, revalidateTag, time-based
CDN caching ยังไม่รองรับ (dynamic: auto) รองรับเต็มรูปแบบ
Personalized content รองรับ (parameterized keys) จำกัด
ใช้ร่วมกันได้ ไม่ได้ (เลือกอย่างใดอย่างหนึ่ง) ไม่ได้
สถานะ ใหม่, กำลังพัฒนา เสถียร, ใช้มานาน

คำแนะนำจากประสบการณ์:

  • ถ้าเริ่มโปรเจกต์ใหม่กับ Next.js 16 → ใช้ Cache Components เลย ไม่ต้องคิดเยอะ
  • ถ้ามีโปรเจกต์เดิมที่ใช้ ISR อยู่ → ค่อยๆ migrate เมื่อพร้อม ไม่ต้องรีบ
  • ถ้าต้องการ CDN-level caching → ยังควรใช้ ISR ในตอนนี้ รอ Cache Components รองรับ CDN เต็มรูปแบบก่อน

9. Best Practices สำหรับ Production

จากประสบการณ์จริงในการใช้ Cache Components กับโปรเจกต์ production มีหลักปฏิบัติที่อยากแชร์:

9.1 อย่า Cache ทุกอย่าง

อันนี้เป็นข้อผิดพลาดที่พบบ่อยมาก Cache เฉพาะสิ่งที่มีต้นทุนในการ fetch สูงหรือถูกเรียกบ่อยๆ ก็พอ อย่า cache ข้อมูลที่เปลี่ยนทุก request อย่าง session data ที่ต้องตรวจสอบทุกครั้ง

9.2 ระวังเรื่อง Cache Key Explosion

ทุก argument ที่ต่างกันจะสร้าง cache entry ใหม่ ลองนึกภาพดู:

// ⚠️ ระวัง: ถ้ามีสินค้า 100,000 รายการ = cache entries 100,000 รายการ
async function getProduct(id: string) {
  'use cache'
  cacheLife('days')
  return db.product.findUnique({ where: { id } })
}

ในกรณีที่มีข้อมูลจำนวนมากแบบนี้ ลองพิจารณาใช้ use cache: remote เพื่อไม่ให้ memory ของ server เต็มจนล่ม

9.3 ห้ามใช้ Dynamic APIs ใน use cache scope

อันนี้เจอบ่อยมากโดยเฉพาะคนที่เพิ่ง migrate มาจากโค้ดเดิม:

// ❌ ผิด — cookies() ใช้ใน 'use cache' ไม่ได้
async function getUserData() {
  'use cache'
  const cookieStore = await cookies() // Error!
  const token = cookieStore.get('token')
  return fetchUser(token)
}

// ✅ ถูก — ส่ง dynamic data เป็น argument
async function getUserData(token: string) {
  'use cache'
  cacheTag('user-data')
  return fetchUser(token)
}

// ใช้งาน
export default async function Page() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')?.value
  const user = await getUserData(token!)
  return <Profile user={user} />
}

9.4 ใช้ tag ให้เป็นระบบ

กำหนดรูปแบบ tag ที่ชัดเจนตั้งแต่แรก แล้วจะขอบคุณตัวเองทีหลัง:

// ตัวอย่างระบบ tag ที่ดี
cacheTag('products')                    // invalidate สินค้าทั้งหมด
cacheTag(`product-${id}`)              // invalidate สินค้าเฉพาะตัว
cacheTag(`category-${categoryId}`)     // invalidate ตาม category
cacheTag(`user-${userId}-orders`)      // invalidate orders ของ user เฉพาะคน

10. ตัวอย่างจริง: สร้างหน้า E-commerce ด้วย Cache Components

มาดูตัวอย่างการใช้ Cache Components ในสถานการณ์จริงกันบ้าง — หน้าสินค้าของ e-commerce ที่มีทั้ง static content และ dynamic content ผสมกัน:

// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { ProductInfo } from '@/components/ProductInfo'
import { ProductReviews } from '@/components/ProductReviews'
import { RelatedProducts } from '@/components/RelatedProducts'
import { AddToCartButton } from '@/components/AddToCartButton'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <main>
      {/* ข้อมูลสินค้า — cache หลายชั่วโมง */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductInfo productId={id} />
      </Suspense>

      {/* ปุ่มเพิ่มลงตะกร้า — ไม่ cache (Client Component) */}
      <AddToCartButton productId={id} />

      {/* รีวิว — cache สั้นๆ */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={id} />
      </Suspense>

      {/* สินค้าที่เกี่ยวข้อง — cache นาน */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={id} />
      </Suspense>
    </main>
  )
}
// components/ProductInfo.tsx
import { cacheLife, cacheTag } from 'next/cache'

export async function ProductInfo({ productId }: { productId: string }) {
  'use cache'
  cacheLife('hours')
  cacheTag(`product-${productId}`, 'products')

  const product = await db.product.findUnique({
    where: { id: productId },
    include: { category: true, brand: true },
  })

  if (!product) {
    cacheLife('minutes')
    return <p>ไม่พบสินค้า</p>
  }

  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>฿{product.price.toLocaleString()}</span>
      <p>หมวดหมู่: {product.category.name}</p>
    </section>
  )
}
// components/RelatedProducts.tsx
import { cacheLife, cacheTag } from 'next/cache'

export async function RelatedProducts({ productId }: { productId: string }) {
  'use cache'
  cacheLife('days')
  cacheTag('related-products', `related-${productId}`)

  const product = await db.product.findUnique({
    where: { id: productId },
    select: { categoryId: true },
  })

  const related = await db.product.findMany({
    where: {
      categoryId: product?.categoryId,
      id: { not: productId },
    },
    take: 4,
  })

  return (
    <section>
      <h2>สินค้าที่เกี่ยวข้อง</h2>
      <div>
        {related.map((item) => (
          <ProductCard key={item.id} product={item} />
        ))}
      </div>
    </section>
  )
}

สังเกตว่าแต่ละ component มี cache strategy ที่ต่างกัน นี่แหละคือพลังที่แท้จริงของ Cache Components — คุณ cache ได้ในระดับ component ไม่ต้อง cache ทั้งหน้าแบบ all-or-nothing อีกต่อไป

FAQ — คำถามที่พบบ่อย

use cache ต้องเปิดใช้งานอย่างไร?

ต้องเพิ่ม cacheComponents: true ใน next.config.ts ก่อน จากนั้นก็ใช้ directive 'use cache' ในโค้ดได้เลย เป็นฟีเจอร์ opt-in ที่ไม่ได้เปิดมาให้ตั้งแต่แรก

use cache ใช้ร่วมกับ ISR ได้ไหม?

ไม่ได้ครับ เมื่อเปิด cacheComponents ระบบ ISR แบบเดิมจะถูกปิดอัตโนมัติ Next.js 16 ไม่รองรับทั้งสองระบบพร้อมกัน ถ้ายังต้องการใช้ ISR ก็อย่าเพิ่งเปิด cacheComponents

ทำไมใช้ cookies() หรือ headers() ภายใน use cache scope ไม่ได้?

เพราะ use cache ออกแบบมาให้ cache ข้อมูลที่ไม่ขึ้นกับ request แต่ละครั้ง ถ้าต้องการใช้ข้อมูลจาก cookies หรือ headers ให้อ่านนอก cache scope แล้วส่งเป็น argument เข้าไป หรือจะใช้ 'use cache: private' แทนก็ได้

revalidateTag กับ updateTag ต่างกันอย่างไร?

revalidateTag ใช้ stale-while-revalidate strategy — mark ข้อมูลเป็น stale แต่ยังให้บริการข้อมูลเก่าจนกว่าจะมีข้อมูลใหม่ ส่วน updateTag จะ expire cache ทันที ทำให้ request ถัดไปต้องรอข้อมูลใหม่ ง่ายๆ คือ updateTag เหมาะกับกรณีที่ user ต้องเห็นการเปลี่ยนแปลงทันทีหลังกดบันทึก

ควรใช้ use cache: remote เมื่อไหร่?

เมื่อแอปของคุณ deploy บนหลาย instances ที่ต้องแชร์ cache ร่วมกัน หรือเมื่อมี cache entries มากจนอาจทำให้ memory ของ server เต็ม use cache: remote เหมาะกับข้อมูลที่ access บ่อยแต่ไม่เปลี่ยนแปลงถี่มาก เช่น configuration หรือ product catalog

เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.