Tại sao Next.js thay đổi hoàn toàn cách caching?
Nếu bạn đã từng làm việc với Next.js 14 hoặc 15, chắc hẳn bạn biết cái cảm giác khó chịu khi fetch() tự động cache mà chẳng thèm hỏi ý kiến ai. Dữ liệu hiển thị sai, user phải hard refresh, và bạn thì ngồi mò mẫm qua đống config kiểu export const dynamic, revalidate, fetchCache chỉ để kiểm soát lại mọi thứ. Nói thật, mình đã mất không ít buổi chiều vì chuyện này.
Next.js 16 giải quyết vấn đề này bằng một triết lý hoàn toàn mới: không gì được cache mặc định. Thay vào đó, bạn sẽ dùng directive 'use cache' để chủ động quyết định phần nào cần cache. Nghe đơn giản nhưng đây là thay đổi tư duy khá lớn — và nó chính là nền tảng của Cache Components, tính năng cốt lõi mới nhất trong Next.js 16.
Trong bài viết này, mình sẽ hướng dẫn bạn từ A đến Z cách sử dụng 'use cache', kết hợp với cacheTag để invalidation theo tag, cacheLife để kiểm soát thời gian cache, và cả hai biến thể 'use cache: remote', 'use cache: private' cho những trường hợp đặc thù.
Thiết lập Cache Components trong Next.js 16
Bật cacheComponents trong next.config.ts
Cache Components là tính năng opt-in — bạn phải bật nó trong file cấu hình trước khi dùng được. Nếu đang nâng cấp từ Next.js 15, nhớ xóa experimental.dynamicIO (đã bị loại bỏ rồi) và thay bằng cacheComponents:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Khi bật flag này, Next.js sẽ thay đổi hành vi mặc định: mọi data fetching trong App Router sẽ không được cache trừ khi bạn đánh dấu rõ ràng bằng 'use cache'.
Nghe có vẻ ngược đời so với phiên bản cũ, nhưng thật ra lại an toàn hơn nhiều. Bạn sẽ không bao giờ bị "dính" dữ liệu cũ mà không hiểu tại sao nữa.
Partial Prerendering được bật tự động
Một điều khá hay: khi bạn bật cacheComponents, Partial Prerendering (PPR) cũng tự động được kích hoạt luôn. Nghĩa là Next.js có thể pre-render phần tĩnh (static shell) của trang rồi stream phần dynamic vào sau. Cache Components về cơ bản là cơ chế quyết định phần nào của trang được đưa vào static shell đó — hai tính năng này bổ sung cho nhau rất tốt.
Directive "use cache" — cú pháp và cách dùng
Ba cấp độ áp dụng
Directive 'use cache' hoạt động kiểu giống 'use client' và 'use server' — nó là một chuỗi string literal báo cho compiler biết đoạn code nằm trong một "ranh giới" đặc biệt. Bạn có thể áp dụng ở ba cấp độ khác nhau.
Cấp độ file — cache toàn bộ page hoặc layout:
'use cache'
export default async function BlogPage() {
const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC')
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Cấp độ component — cache từng component riêng biệt (đây là cách mình dùng nhiều nhất):
export async function ProductList({ categoryId }: { categoryId: string }) {
'use cache'
const products = await db.query(
'SELECT * FROM products WHERE category_id = ?',
[categoryId]
)
return (
<ul>
{products.map(p => <li key={p.id}>{p.name} - {p.price}</li>)}
</ul>
)
}
Cấp độ function — cache kết quả của một hàm data fetching:
export async function getCategories() {
'use cache'
const categories = await db.query('SELECT * FROM categories WHERE active = true')
return categories
}
Lưu ý quan trọng: Khi dùng 'use cache' ở cấp độ file, tất cả function exports trong file đó phải là async function. Quên cái này là build sẽ lỗi ngay.
Cache key được tạo tự động
Đây là một trong những điểm mình thích nhất ở 'use cache': compiler sẽ tự động tạo cache key dựa trên arguments và các giá trị closed-over từ scope cha. Bạn không cần phải tự quản lý cache key — mỗi tổ hợp input khác nhau sẽ tạo ra một cache entry riêng biệt.
// Mỗi categoryId khác nhau sẽ có cache entry riêng
export async function getProducts(categoryId: string) {
'use cache'
return await fetchProductsByCategory(categoryId)
}
// getProducts('electronics') → cache entry A
// getProducts('clothing') → cache entry B
So với unstable_cache trước đây — nơi bạn phải tự chỉ định cache key thủ công và rất dễ bị cache collision hoặc stale data — thì đây là cải tiến cực kỳ đáng giá.
cacheLife — kiểm soát thời gian cache
Ba thuộc tính cốt lõi: stale, revalidate, expire
Hàm cacheLife cho phép bạn kiểm soát chính xác thời gian cache thông qua ba thuộc tính:
- stale: Thời gian client sử dụng dữ liệu cache mà không kiểm tra server (đơn vị: giây).
- revalidate: Sau khoảng thời gian này, request tiếp theo sẽ trigger background refresh — nhưng user vẫn nhận dữ liệu cũ ngay lập tức. Đúng kiểu stale-while-revalidate quen thuộc.
- expire: Sau khoảng thời gian này mà không có request nào, cache entry bị xóa hoàn toàn. Request tiếp theo phải chờ dữ liệu mới.
Sử dụng profile có sẵn
Next.js đã cung cấp sẵn các profile mặc định phù hợp với hầu hết use case, nên bạn không cần tự config từ đầu:
import { cacheLife } from 'next/cache'
// Profile 'default' — stale: 5 phút, revalidate: 15 phút
export async function getSettings() {
'use cache'
cacheLife('default')
return await fetchSettings()
}
// Profile 'hours' — cache trong vài giờ
export async function getPopularPosts() {
'use cache'
cacheLife('hours')
return await fetchPopularPosts()
}
// Profile 'days' — cache trong nhiều ngày
export async function getStaticContent() {
'use cache'
cacheLife('days')
return await fetchAboutPageContent()
}
// Profile 'minutes' — cache ngắn hạn
export async function getNotifications() {
'use cache'
cacheLife('minutes')
return await fetchLatestNotifications()
}
Tạo profile tùy chỉnh
Khi các profile mặc định không đáp ứng được nhu cầu, bạn hoàn toàn có thể tạo profile riêng trong next.config.ts. Mình thường tạo ít nhất 2-3 profile tùy chỉnh cho mỗi dự án:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
// Profile cho bài blog — cache 1 ngày, kiểm tra mỗi 15 phút
blog: {
stale: 3600, // 1 giờ trên client
revalidate: 900, // 15 phút trên server
expire: 86400, // Hết hạn sau 1 ngày
},
// Profile cho sản phẩm e-commerce — cần cập nhật thường xuyên hơn
product: {
stale: 300, // 5 phút trên client
revalidate: 60, // 1 phút trên server
expire: 3600, // Hết hạn sau 1 giờ
},
// Profile cho nội dung tĩnh — hiếm khi thay đổi
static: {
stale: 86400, // 1 ngày trên client
revalidate: 43200, // 12 giờ trên server
expire: 604800, // Hết hạn sau 1 tuần
},
},
}
export default nextConfig
Rồi dùng profile tùy chỉnh trong code đơn giản như sau:
import { cacheLife } from 'next/cache'
export async function getBlogPost(slug: string) {
'use cache'
cacheLife('blog')
return await db.posts.findOne({ slug })
}
Profile động tại runtime
Đây là phần mà mình thấy thật sự ấn tượng: bạn có thể quyết định thời gian cache dựa trên dữ liệu thực tế tại runtime. Ví dụ, CMS cho phép editor đặt thời gian revalidate riêng cho mỗi bài viết:
import { cacheLife, cacheTag } from 'next/cache'
async function getArticleContent(slug: string) {
'use cache'
const article = await fetchArticle(slug)
cacheTag(`article-${slug}`)
if (!article) {
cacheLife('minutes') // Không tìm thấy? Cache ngắn để thử lại sớm
return null
}
// Dùng thời gian revalidate từ CMS
cacheLife({
revalidate: article.revalidateSeconds ?? 3600,
})
return article.data
}
Các thuộc tính không được chỉ định sẽ tự động kế thừa từ profile default. Khá tiện lợi.
cacheTag — gắn tag và invalidation theo yêu cầu
Cách gắn tag cho cache entries
Hàm cacheTag cho phép bạn gắn "nhãn" cho các cache entries, rồi xóa cache có chọn lọc khi dữ liệu thay đổi. Đây là bước tiến lớn so với trước đây khi cache tagging chỉ hoạt động với fetch(). Giờ thì bạn có thể tag cache cho bất kỳ thứ gì — database queries, file system operations, API calls, tuỳ bạn.
import { cacheTag } from 'next/cache'
export async function getProducts(categoryId: string) {
'use cache'
cacheTag('products', `category-${categoryId}`)
const products = await db.query(
'SELECT * FROM products WHERE category_id = ?',
[categoryId]
)
return products
}
Giới hạn cần nhớ: Mỗi tag tối đa 256 ký tự, mỗi cache entry gắn được tối đa 128 tag. Thực tế thì hiếm khi bạn cần nhiều đến vậy.
Invalidation với revalidateTag
Khi dữ liệu thay đổi — ví dụ admin cập nhật sản phẩm — bạn gọi revalidateTag trong Server Action hoặc Route Handler:
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(productId: string, data: ProductData) {
await db.products.update(productId, data)
// Xóa cache cho tất cả sản phẩm
revalidateTag('products', 'max')
// Hoặc xóa cache cho category cụ thể
revalidateTag(`category-${data.categoryId}`, 'max')
}
Tham số thứ hai 'max' bật chế độ stale-while-revalidate: user hiện tại vẫn nhận dữ liệu cũ ngay lập tức, trong khi dữ liệu mới được fetch ở background. Request tiếp theo sẽ nhận dữ liệu mới.
updateTag — cho trường hợp "đọc ngay sau khi ghi"
Có những lúc bạn cần user nhìn thấy thay đổi ngay lập tức sau khi submit form (kiểu read-your-own-writes). Lúc đó hãy dùng updateTag thay vì revalidateTag:
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(userId: string, formData: FormData) {
const name = formData.get('name') as string
const bio = formData.get('bio') as string
await db.users.update(userId, { name, bio })
// Expire cache ngay lập tức — user sẽ thấy thay đổi ngay
updateTag(`user-${userId}`)
}
Khác biệt chính giữa hai cách:
revalidateTag: Dùng trong Server Actions và Route Handlers. Hỗ trợ stale-while-revalidate. Phù hợp cho nội dung tĩnh.updateTag: Chỉ dùng trong Server Actions. Expire cache ngay lập tức. Phù hợp cho form submissions và tương tác trực tiếp từ user.
Ví dụ thực tế: blog platform với Cache Components
Nói lý thuyết nhiều cũng chán. Hãy cùng xem một ví dụ hoàn chỉnh — xây dựng blog platform tận dụng tối đa Cache Components nhé.
Cấu hình next.config.ts
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
blog: {
stale: 3600,
revalidate: 900,
expire: 86400,
},
sidebar: {
stale: 7200,
revalidate: 3600,
expire: 172800,
},
},
}
export default nextConfig
Data fetching layer
// lib/data/posts.ts
import { cacheTag, cacheLife } from 'next/cache'
export async function getAllPosts() {
'use cache'
cacheTag('posts')
cacheLife('blog')
const posts = await db.query(`
SELECT p.*, u.name as author_name
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.published = true
ORDER BY p.created_at DESC
`)
return posts
}
export async function getPostBySlug(slug: string) {
'use cache'
cacheTag('posts', `post-${slug}`)
cacheLife('blog')
const post = await db.query(
'SELECT * FROM posts WHERE slug = ? AND published = true',
[slug]
)
return post[0] ?? null
}
export async function getPopularTags() {
'use cache'
cacheTag('tags')
cacheLife('sidebar')
const tags = await db.query(`
SELECT t.name, COUNT(pt.post_id) as post_count
FROM tags t
JOIN post_tags pt ON t.id = pt.tag_id
GROUP BY t.id
ORDER BY post_count DESC
LIMIT 20
`)
return tags
}
Page component kết hợp cached và dynamic content
Đây là phần thú vị — bạn sẽ thấy cách cached components và dynamic components sống chung trên cùng một trang:
// app/blog/page.tsx
import { Suspense } from 'react'
import { getAllPosts } from '@/lib/data/posts'
import { DynamicSidebar } from '@/components/DynamicSidebar'
import { PostListSkeleton } from '@/components/skeletons'
export default async function BlogPage() {
return (
<div className="grid grid-cols-3 gap-8">
<main className="col-span-2">
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</main>
<aside>
<Suspense fallback={<div>Đang tải...</div>}>
<DynamicSidebar />
</Suspense>
</aside>
</div>
)
}
async function PostList() {
'use cache'
cacheLife('blog')
cacheTag('posts')
const posts = await getAllPosts()
return (
<div className="space-y-6">
{posts.map(post => (
<article key={post.id} className="border-b pb-6">
<h2 className="text-2xl font-bold">
<a href={`/blog/${post.slug}`}>{post.title}</a>
</h2>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
<span className="text-sm text-gray-400">{post.author_name}</span>
</article>
))}
</div>
)
}
Server Action xử lý invalidation
// app/admin/actions.ts
'use server'
import { revalidateTag, updateTag } from 'next/cache'
export async function publishPost(postId: string) {
await db.posts.update(postId, { published: true })
// Revalidate danh sách bài viết (stale-while-revalidate)
revalidateTag('posts', 'max')
}
export async function updatePostContent(slug: string, content: string) {
await db.posts.update({ slug }, { content })
// User admin cần thấy thay đổi ngay
updateTag(`post-${slug}`)
}
export async function deletePost(postId: string, slug: string) {
await db.posts.delete(postId)
// Xóa cache cho bài viết cụ thể và danh sách
revalidateTag('posts', 'max')
revalidateTag(`post-${slug}`, 'max')
}
Biến thể "use cache: remote" và "use cache: private"
use cache: remote — cache phân tán cho production
Mặc định, 'use cache' sử dụng in-memory LRU storage. Cache sẽ mất khi server restart, và trong môi trường serverless thì mỗi instance có cache riêng — khá bất tiện. Đây là lúc 'use cache: remote' phát huy tác dụng:
export async function getGlobalConfig() {
'use cache: remote'
cacheTag('config')
cacheLife('days')
return await fetchConfigFromDatabase()
}
Khi dùng biến thể này, dữ liệu cache được lưu trữ trong remote cache (Redis, KV database) thay vì in-memory. Lợi ích thì rõ ràng:
- Cache được chia sẻ giữa tất cả server instances.
- Cache tồn tại qua server restart.
- Giảm tải đáng kể cho upstream services như CMS, database.
Tất nhiên cũng có tradeoff: thêm network latency khi lookup cache và chi phí hạ tầng cho remote storage. Bạn nên cân nhắc kỹ trước khi dùng cho mọi thứ.
Cấu hình custom cache handler
Để sử dụng 'use cache: remote', bạn cần cấu hình một cache handler. Trên Vercel thì khỏi lo — nó được xử lý tự động. Nhưng nếu bạn self-host, bạn cần tạo custom handler:
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
cacheHandlers: {
remote: require.resolve('./lib/cache/redis-handler'),
},
}
use cache: private — cache dữ liệu theo user
Directive 'use cache: private' giải quyết một bài toán đặc biệt: cache dữ liệu cá nhân hóa cho từng user. Khác với 'use cache' thông thường, 'use cache: private' cho phép truy cập trực tiếp các runtime API như cookies() và headers():
import { cookies } from 'next/headers'
import { cacheTag, cacheLife } from 'next/cache'
export async function getUserRecommendations(productId: string) {
'use cache: private'
cacheTag(`recommendations-${productId}`)
cacheLife({ stale: 60 })
const sessionId = (await cookies()).get('session-id')?.value || 'guest'
return await fetchPersonalizedRecommendations(sessionId, productId)
}
Lưu ý: Kết quả từ 'use cache: private' chỉ được cache trong bộ nhớ trình duyệt, không tồn tại qua page reload. Và directive này vẫn đang ở giai đoạn experimental, nên hãy dùng cẩn thận trong production.
Migration từ unstable_cache sang "use cache"
Nếu project hiện tại của bạn đang dùng unstable_cache, thì đây là lúc thích hợp nhất để chuyển đổi. unstable_cache đã chính thức bị thay thế trong Next.js 16, nên sớm muộn gì bạn cũng phải migration thôi.
So sánh trước và sau
Code cũ với unstable_cache:
import { unstable_cache } from 'next/cache'
const getCachedProducts = unstable_cache(
async (categoryId: string) => {
return await db.products.findMany({
where: { categoryId },
})
},
['products'], // cache key thủ công
{ revalidate: 3600, tags: ['products'] }
)
Code mới với "use cache":
import { cacheTag, cacheLife } from 'next/cache'
export async function getProducts(categoryId: string) {
'use cache'
cacheTag('products')
cacheLife({ revalidate: 3600 })
return await db.products.findMany({
where: { categoryId },
})
}
Sự khác biệt nhìn là thấy: code mới gọn gàng hơn hẳn, không cần wrapper function, cache key tự động tạo từ arguments, và bạn còn có thể cache cả components chứ không chỉ function results. Thật sự là một bước tiến lớn.
Debug và xử lý lỗi thường gặp
Bật debug mode
Để theo dõi hành vi cache trong quá trình phát triển, dùng biến môi trường NEXT_PRIVATE_DEBUG_CACHE:
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
Biến này sẽ log chi tiết mọi cache hit, miss, và revalidation trong console. Rất hữu ích khi bạn cần hiểu tại sao dữ liệu không được cache (hoặc cache quá lâu).
Lỗi build timeout
Đây là lỗi mà khá nhiều người gặp phải khi mới dùng 'use cache'. Nếu build bị treo, nguyên nhân thường là bạn đang truy cập dữ liệu dynamic (cookies, headers, searchParams) bên trong cached scope. Error message sẽ trông kiểu này:
Error: Filling a cache during prerender timed out, likely because
request-specific arguments such as params, searchParams, cookies()
or dynamic data were used inside 'use cache'.
Cách khắc phục: Đọc dữ liệu dynamic bên ngoài cached scope rồi truyền vào dưới dạng arguments. Xem ví dụ cụ thể:
// ❌ Sai — truy cập cookies bên trong 'use cache'
export async function getUserData() {
'use cache'
const session = await cookies() // Build sẽ timeout!
return await fetchUserData(session.get('userId'))
}
// ✅ Đúng — truyền giá trị dynamic qua argument
export async function getUserData(userId: string) {
'use cache'
cacheTag(`user-${userId}`)
return await fetchUserData(userId)
}
// Gọi hàm từ page component (không cached)
export default async function ProfilePage() {
const session = await cookies()
const userId = session.get('userId')?.value
const userData = await getUserData(userId!)
return <Profile data={userData} />
}
Cache không hoạt động trong dynamic routes
Vấn đề này cũng khá phổ biến. Nếu bạn dùng 'use cache' trong dynamic route (như /blog/[slug]) nhưng cache dường như không hoạt động, hãy kiểm tra mấy điều sau:
- Đảm bảo
cacheComponents: trueđã được bật trongnext.config.ts. - Kiểm tra xem có
export const dynamic = 'force-dynamic'trong layout hoặc page không — cấu hình này sẽ override'use cache', và đây là nguyên nhân số 1 mà mọi người quên. - Wrap component cached trong
<Suspense>boundary để Next.js có thể tách phần cached ra khỏi phần dynamic.
Best practices khi sử dụng Cache Components
1. Tách biệt cached và dynamic logic
Đừng trộn lẫn dữ liệu tĩnh và dữ liệu cần cookies/headers trong cùng một cached scope. Tạo các function riêng biệt cho từng loại — đây là quy tắc vàng khi dùng Cache Components:
// ✅ Tốt — tách riêng cached và uncached
async function CachedProductInfo({ id }: { id: string }) {
'use cache'
cacheTag(`product-${id}`)
const product = await getProduct(id)
return <ProductDetails product={product} />
}
async function DynamicCartButton({ productId }: { productId: string }) {
// Không cache — cần truy cập session
const session = await getSession()
return <AddToCartButton productId={productId} userId={session.userId} />
}
// Kết hợp trong page
export default async function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
<Suspense fallback={<ProductSkeleton />}>
<CachedProductInfo id={params.id} />
</Suspense>
<Suspense fallback={<ButtonSkeleton />}>
<DynamicCartButton productId={params.id} />
</Suspense>
</div>
)
}
2. Luôn dùng Suspense với cached components
Khi cacheComponents được bật, cached components sẽ được đưa vào static shell của PPR. Bạn cần wrap chúng trong <Suspense> để Next.js có thể stream phần dynamic vào sau mà không block toàn bộ page. Bỏ qua bước này là bạn sẽ mất lợi ích lớn nhất của PPR.
3. Đặt cacheLife phù hợp với từng loại dữ liệu
Không phải dữ liệu nào cũng cần cache giống nhau. Đây là gợi ý nhanh:
- Nội dung tĩnh (about, terms, FAQs):
cacheLife('days')hoặccacheLife('weeks') - Blog posts:
cacheLife('hours')hoặc profile tùy chỉnh - Sản phẩm e-commerce:
cacheLife('minutes')kết hợpcacheTagđể invalidation nhanh - Dashboard data: Không cache, hoặc
cacheLife({ stale: 30, revalidate: 10 })
4. Dùng cacheTag có chiến lược
Gắn tag theo nhóm dữ liệu logic, không theo tên function. Cách này giúp bạn invalidate chính xác những gì cần xóa mà không làm ảnh hưởng tới cache khác:
// Tag theo entity — dễ invalidate khi update
cacheTag('products') // Tất cả sản phẩm
cacheTag(`product-${id}`) // Sản phẩm cụ thể
cacheTag(`category-${categoryId}`) // Theo category
// Không nên tag quá generic
cacheTag('data') // ❌ Quá chung chung
Câu hỏi thường gặp (FAQ)
"use cache" có thay thế hoàn toàn ISR không?
Không hẳn. 'use cache' hoạt động ở cấp độ function/component — nó cache kết quả của data fetching và rendering. ISR truyền thống (với revalidate trong fetch) vẫn hoạt động cho full-route caching trên CDN. Tuy nhiên, 'use cache' linh hoạt hơn rất nhiều vì cho phép bạn cache từng phần riêng biệt thay vì phải cache toàn bộ page. Theo mình, trong hầu hết trường hợp mới, 'use cache' kết hợp cacheTag là lựa chọn tốt hơn.
Tôi có thể dùng "use cache" với Client Components không?
Không trực tiếp. 'use cache' chỉ hoạt động trong Server Components và server-side functions. Nhưng bạn có thể tạo Server Component được cache bọc quanh Client Component — dữ liệu được fetch và cache trên server, rồi truyền xuống Client Component dưới dạng props. Cách này hoạt động rất ổn.
Cache entries được lưu ở đâu khi self-host?
Mặc định là in-memory LRU. Cache sẽ mất khi process restart và không chia sẻ giữa các instances. Để giải quyết, dùng 'use cache: remote' với custom cache handler (Redis, Memcached) được cấu hình qua cacheHandlers trong next.config.ts.
Làm sao biết cache đang hoạt động đúng không?
Chạy ứng dụng với NEXT_PRIVATE_DEBUG_CACHE=1 để xem log chi tiết. Bạn cũng có thể kiểm tra response header x-nextjs-stale-time để xác nhận thời gian cache trên client. Trong production, nên monitor cache hit ratio để đảm bảo cấu hình đang hoạt động như mong đợi.
Sự khác biệt giữa revalidateTag và revalidatePath là gì?
revalidateTag xóa cache dựa trên tag — phù hợp khi bạn muốn invalidate theo nhóm logic (ví dụ tất cả sản phẩm trong một category). revalidatePath xóa cache của một route cụ thể — phù hợp khi bạn biết chính xác URL nào cần refresh. Nói chung, revalidateTag linh hoạt hơn và là lựa chọn được khuyến khích khi dùng Cache Components.