Hướng dẫn Revalidation động trong Next.js App Router

Table of Contents
Nếu gần đây bạn đã chuyển sang Next.js App Router, có lẽ bạn đã gặp phải tính năng gây tranh cãi nhất của framework này: cơ chế caching mặc định rất mạnh mẽ. Next.js cache dữ liệu mạnh đến mức trang web của bạn sẽ tải trong vòng mili giây trên toàn cầu—nhưng để hiển thị dữ liệu đã cập nhật thì có thể cảm giác như đang vật lộn với một con gấu.
Tôi đã dành tháng đầu tiên với App Router để vật lộn với dữ liệu cũ. Sau khi đào sâu vào mã nguồn và triển khai một số ứng dụng có lưu lượng truy cập cao, cuối cùng tôi cũng hiểu được kiến trúc caching đa tầng.
Trong hướng dẫn này, chúng ta sẽ bỏ qua những lý thuyết chung chung và đi sâu vào cách Next.js Data Cache thực sự hoạt động, cách revalidateTag xóa các mục cache cụ thể theo yêu cầu, và cách ngăn dữ liệu cũ ám ảnh các cache router phía client của bạn. Chúng ta cũng sẽ đề cập đến caching cơ sở dữ liệu với unstable_cache và cách kết nối các trình xử lý revalidation webhook mạnh mẽ.
Giải mã Lớp Next.js Data Cache
Next.js Data Cache không phải là cache trình duyệt tiêu chuẩn của bạn. Nó nằm trên máy chủ Node.js của bạn (hoặc cơ sở hạ tầng biên của Vercel) và lưu trữ liên tục các phản hồi fetch HTTP qua các yêu cầu và thậm chí cả các lần triển khai. Khi Server Component của bạn thực hiện một lệnh gọi fetch(), Next.js sẽ chặn nó, kiểm tra Data Cache và bỏ qua hoàn toàn mạng nếu tìm thấy một hit.

Để thành thạo quản lý cache, các kỹ sư phần mềm phải hiểu bốn lớp caching riêng biệt hoạt động bên trong Next.js App Router:
+-----------------------------------------------------------------------------------+
| Next.js App Router Caching Sub-system Architecture |
+-----------------------------------------------------------------------------------+
| 1. Request Memoization | Server request scope | Deduplicates fetch calls in 1 render |
| 2. Data Cache | Persistent server storage | Stores fetch responses across requests|
| 3. Full Route Cache | Server build storage | Stores HTML & RSC payloads for static |
| 4. Router Cache | Client browser memory | Stores visited route segments in SPA |
+-----------------------------------------------------------------------------------+
Hãy cùng xem cách cấu hình revalidation dựa trên thời gian và gán tag bên trong một yêu cầu fetch của Server Component:
// app/products/[slug]/page.tsx
import { notFound } from 'next/navigation';
type Product = {
id: string;
slug: string;
name: string;
price: number;
description: string;
inventoryCount: number;
category: string;
};
async function getProductData(slug: string): Promise<Product | null> {
const response = await fetch(`https://api.example.com/products/${slug}`, {
// Assign cache tags and define 1-hour background revalidation interval
next: {
tags: [`product:${slug}`, 'products', 'inventory'],
revalidate: 3600
}
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error('Failed to fetch product data from backend API.');
}
return response.json();
}
async function getRelatedProducts(category: string): Promise<Product[]> {
const response = await fetch(`https://api.example.com/products?category=${category}`, {
next: {
tags: [`category:${category}`, 'products'],
revalidate: 7200
}
});
if (!response.ok) {
return [];
}
return response.json();
}
export default async function ProductDetailPage({
params
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const product = await getProductData(slug);
if (!product) {
notFound();
}
const related = await getRelatedProducts(product.category);
return (
<article className="product-page">
<header className="product-header">
<h1>{product.name}</h1>
<p className="price">${product.price.toFixed(2)}</p>
<p className="inventory">Available Stock: {product.inventoryCount}</p>
</header>
<section className="description-section">
<p className="description">{product.description}</p>
</section>
<section className="related-section">
<h2>Related Products in {product.category}</h2>
<div className="related-grid">
{related.map((item) => (
<div key={item.id} className="related-card">
<h3>{item.name}</h3>
<p>${item.price.toFixed(2)}</p>
</div>
))}
</div>
</section>
</article>
);
}
Lưu ý cách next.tags gán các chuỗi ngữ nghĩa cho đối tượng phản hồi đã được cache. Bạn có thể gán nhiều tag cho một yêu cầu fetch duy nhất, cho phép vô hiệu hóa chi tiết cho một sản phẩm cụ thể (product:keyboard-v2) cùng với vô hiệu hóa hàng loạt rộng rãi cho danh sách sản phẩm (products).
Hãy cùng xem cách Request Memoization hoạt động cùng với Data Cache trong một chu kỳ render server duy nhất:
// Both Layout and Page components call getProductData('keyboard-v2')
// Request Memoization ensures ONLY ONE HTTP request is executed on the server!
export async function HeaderLayout({ children }: { children: React.ReactNode }) {
const product = await getProductData('keyboard-v2');
return (
<div>
<nav>Current Item: {product?.name}</nav>
{children}
</div>
);
}
Nếu bạn đang truy vấn cơ sở dữ liệu trực tiếp bằng Prisma hoặc Drizzle ORM mà không có HTTP fetch, Next.js cung cấp tiện ích unstable_cache để đạt được hành vi caching tag tương tự:
// lib/cachedQueries.ts
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
export const getCachedProductBySlug = (slug: string) =>
unstable_cache(
async () => {
return db.product.findUnique({
where: { slug }
});
},
[`product-query-${slug}`], // Cache key array
{
tags: [`product:${slug}`, 'products'],
revalidate: 3600
}
)();
Việc gói các truy vấn cơ sở dữ liệu trong unstable_cache mang lại cho các lệnh gọi ORM của bạn khả năng xóa tag chính xác như các yêu cầu fetch HTTP tiêu chuẩn. Bạn không cần phải viết lại các lớp trừu tượng cơ sở dữ liệu hiện có của mình để hưởng lợi từ việc vô hiệu hóa tag.
revalidateTag Cung cấp Khả năng Xóa Cache Theo Yêu cầu Chi tiết Như Thế Nào?
Hàm revalidateTag cung cấp khả năng xóa cache chi tiết bằng cách vô hiệu hóa tất cả các mục fetch đã được cache được gán cho các định danh tag cụ thể ngay lập tức khỏi bộ nhớ máy chủ. Thay vì chờ các bộ hẹn giờ revalidation dựa trên thời gian hết hạn, việc gọi revalidateTag('tag-name') sẽ xóa các mục phù hợp khỏi Data Cache và vô hiệu hóa các mục phụ thuộc trong Full Route Cache đồng thời.

Khả năng này rất cần thiết khi xây dựng các quy trình làm việc Headless CMS hoặc các nền tảng thương mại điện tử nơi các cập nhật giá hoặc chỉnh sửa nội dung phải được xuất bản ngay lập tức trên các tuyến đường toàn cầu.
Hãy cùng xem cách Server Action xóa các tag cache sản phẩm sau một thay đổi tồn kho:
// actions/inventoryActions.ts
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/database';
export async function updateProductInventory(
productId: string,
productSlug: string,
newStockCount: number
) {
try {
await db.product.update({
where: { id: productId },
data: { inventoryCount: newStockCount }
});
// Instantly purge specific product tag and global inventory tag
revalidateTag(`product:${productSlug}`);
revalidateTag('inventory');
return { success: true, message: 'Inventory updated and cache purged successfully!' };
} catch (error) {
return { success: false, message: 'Database write failed during inventory update.' };
}
}
Bây giờ, hãy kiểm tra cách một Webhook Route Handler bên ngoài xử lý các sự kiện cập nhật nội dung đến từ một Headless CMS như Contentful hoặc Sanity:
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.CMS_WEBHOOK_SECRET) {
return NextResponse.json({ message: 'Unauthorized webhook request.' }, { status: 401 });
}
try {
const body = await request.json();
const { entityType, slug, tags } = body;
if (tags && Array.isArray(tags)) {
// Purge array of incoming webhook tags
tags.forEach((tag: string) => revalidateTag(tag));
} else if (slug) {
revalidateTag(`${entityType}:${slug}`);
} else {
revalidateTag(entityType);
}
return NextResponse.json({
revalidated: true,
timestamp: new Date().toISOString()
});
} catch (err) {
return NextResponse.json(
{ message: 'Error processing webhook revalidation payload.' },
{ status: 500 }
);
}
}
Bằng cách tách rời việc vô hiệu hóa cache thành các endpoint webhook chuyên dụng, CMS backend của bạn có thể kích hoạt cập nhật trang web tức thì bất cứ khi nào biên tập viên xuất bản thay đổi mà không cần xây dựng lại các tuyến đường trang tĩnh. Bạn sẽ không cần phải kích hoạt triển khai lại toàn bộ trang web cho các cập nhật nội dung đơn giản.
Ngoài ra, revalidateTag thực thi không đồng bộ mà không chặn phản hồi API đến. Máy chủ xóa các khóa cache phù hợp trong bộ nhớ và xếp hàng render lại nền cho các trang tĩnh phụ thuộc.
Khi gỡ lỗi vô hiệu hóa tag trong quá trình phát triển cục bộ, hãy kiểm tra bảng điều khiển terminal của bạn để tìm các thông báo trạng thái [Cache] Tag "product:123" invalidated. Next.js in các hoạt động cache chi tiết khi chạy máy chủ dev với các cờ debug được bật. Bạn có thể xác minh rằng việc vô hiệu hóa tag xóa các khóa cache mong muốn trước khi đẩy thay đổi lên các triển khai sản xuất.
Các Kỹ sư Có Thể Ngăn Chặn Dữ liệu Cũ Trong Dynamic Routes và Client Router Caches Bằng Cách Nào?
Các kỹ sư ngăn chặn dữ liệu cũ bằng cách ghép các tag revalidation máy chủ với các lệnh gọi làm mới router để xóa các cache router trong bộ nhớ phía client. Client Router Cache lưu trữ các phân đoạn payload React Server Component (RSC) đã truy cập trong bộ nhớ trình duyệt. Ngay cả khi revalidateTag vô hiệu hóa Data Cache của máy chủ, người dùng điều hướng trở lại một tuyến đường client đã truy cập gần đây vẫn có thể xem dữ liệu cũ từ bộ nhớ trình duyệt cho đến khi cache client hết hạn.

Để giải quyết tình trạng trễ cache phía client, các nhà phát triển frontend phải gọi router.refresh() từ next/navigation sau các thay đổi trạng thái phía client. Việc gọi router.refresh() buộc trình duyệt phải loại bỏ payload RSC đã được cache và lấy HTML được render mới từ máy chủ.
Hãy để tôi chỉ cho bạn một client component kết hợp thực thi Server Action với router.refresh():
// components/InventoryUpdater.tsx
'use client';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { updateProductInventory } from '@/actions/inventoryActions';
type InventoryUpdaterProps = {
productId: string;
productSlug: string;
initialStock: number;
};
export function InventoryUpdater({
productId,
productSlug,
initialStock
}: InventoryUpdaterProps) {
const router = useRouter();
const [stock, setStock] = useState(initialStock);
const [isPending, startTransition] = useTransition();
const handleStockUpdate = async (newCount: number) => {
setStock(newCount);
startTransition(async () => {
const result = await updateProductInventory(productId, productSlug, newCount);
if (result.success) {
// Force the client browser router cache to refresh fresh server RSC payload
router.refresh();
} else {
// Rollback stock state on server write failure
setStock(initialStock);
alert(result.message);
}
});
};
return (
<div className="stock-controls">
<span>Current Inventory: {stock}</span>
<button
onClick={() => handleStockUpdate(stock + 1)}
disabled={isPending}
>
+ Add Stock
</button>
<button
onClick={() => handleStockUpdate(Math.max(0, stock - 1))}
disabled={isPending || stock === 0}
>
- Decrease Stock
</button>
</div>
);
}
Ngoài ra, nếu một phân đoạn tuyến đường động phụ thuộc vào các tiêu đề yêu cầu hoặc cookie trực tiếp, hãy thực thi render động ở cấp phân đoạn bằng cách định nghĩa các tham số phân đoạn tuyến đường:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export default async function LiveDashboardPage() {
// Page skips static route caching entirely and renders dynamically on every request
return <div>Live Enterprise Analytics Stream</div>;
}
Cấu hình export const dynamic = 'force-dynamic' đảm bảo rằng người dùng xem dữ liệu trực tiếp trên mỗi lần truy cập trang, loại bỏ rủi ro caching cho các bảng điều khiển người dùng giao dịch. Bạn sẽ thấy rằng việc kết hợp caching tĩnh cho các trang tiếp thị với render động cho các bảng điều khiển người dùng tạo ra một sự cân bằng kiến trúc lý tưởng. Các nhóm phần mềm đạt được cả tốc độ cực nhanh và dữ liệu hoàn toàn mới.
Hãy cùng xem cách các chức năng tìm nạp nền stale-while-revalidate hoạt động. Khi một yêu cầu đến một trang đang được revalidate sau khi bộ hẹn giờ hết hạn, Next.js ngay lập tức phục vụ phản hồi cũ đã được cache trong khi khởi động một worker nền để tìm nạp dữ liệu mới. Khi quá trình tìm nạp nền hoàn tất, các yêu cầu tiếp theo sẽ tự động nhận được nội dung cập nhật. Bạn không cần phải viết các worker hàng đợi nền tùy chỉnh vì framework quản lý các tác vụ revalidation không đồng bộ ngay từ đầu.
Các Thực Tiễn Tốt Nhất Về Kiến Trúc Vô Hiệu Hóa Cache Cấp Doanh Nghiệp Là Gì?
Các thực tiễn tốt nhất về vô hiệu hóa cache cấp doanh nghiệp bao gồm thiết lập các quy ước đặt tên tag nhất quán, tách rời các webhook CMS khỏi các trình xử lý tuyến đường và kiểm tra tỷ lệ hit cache. Trong các tổ chức doanh nghiệp lớn với nhiều nhóm kỹ thuật tự chủ, việc đặt tên tag không có cấu trúc nhanh chóng dẫn đến lỗi xung đột tag hoặc bỏ sót các lần xóa cache.
Hãy cùng xem xét các hướng dẫn kiến trúc thiết yếu cho caching Next.js cấp doanh nghiệp:
-
Áp dụng Tiêu chuẩn Đặt tên Tag Phân cấp: Sử dụng các mẫu không gian tên có cấu trúc như
domain:entity:id(ví dụ:store:product:9842). Việc đặt tên phân cấp làm cho ý định của tag rõ ràng và ngăn chặn các xung đột ngẫu nhiên trên các microservice. -
Phân biệt Tag Thực thể với Tag Danh sách: Duy trì các tag riêng biệt cho các bản ghi riêng lẻ (
user:42) và các bộ sưu tập tổng hợp (users:all). Việc xóa một mục người dùng duy nhất không nên xóa tất cả các danh sách thư mục người dùng đã được cache một cách không cần thiết. -
Ghi nhật ký Kích hoạt Revalidation Webhook: Ghi nhật ký các payload webhook đến và dấu thời gian thực thi bên trong các công cụ giám sát APM máy chủ như Datadog hoặc Sentry. Bạn sẽ nhanh chóng theo dõi các cập nhật bị thiếu khi chữ ký webhook không thành công.
-
Triển khai Bộ điều hợp Cache Redis Tùy chỉnh cho Triển khai Phân tán: Khi triển khai Next.js trên nhiều phiên bản container, hãy cấu hình các trình xử lý lưu trữ cache Redis tùy chỉnh bằng cách sử dụng
@nexus/cacheđể đồng bộ hóa trạng thái Data Cache trên tất cả các node máy chủ. -
Kiểm tra Tỷ lệ Hit Cache trong Sản xuất: Giám sát nhật ký máy chủ để tìm các tiêu đề cache HIT so với MISS. Tỷ lệ miss cache cao cho thấy các khoảng thời gian revalidation quá mạnh hoặc thiếu các liên kết tag.
Đây là cách một Next.js Middleware tùy chỉnh có thể thêm các tiêu đề cache tùy chỉnh vào các phản hồi API hạ nguồn:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
if (request.nextUrl.pathname.startsWith('/api/public')) {
// Enforce Edge CDN caching headers for public API endpoints
response.headers.set(
'Cache-Control',
'public, s-maxage=3600, stale-while-revalidate=86400'
);
}
return response;
}
Cấu hình các tiêu đề caching biên tùy chỉnh bên trong Next.js Middleware đảm bảo rằng các endpoint công cộng có lưu lượng truy cập cao phục vụ các phản hồi đã được cache trực tiếp từ các vị trí biên CDN. Bạn sẽ giảm chi phí tính toán máy chủ backend hơn 90% trong các chiến dịch tiếp thị có lưu lượng truy cập cao.
Đánh đổi Kiến trúc: revalidatePath so với revalidateTag
+--------------------------------+-----------------------------------+-----------------------------------+
| Feature Aspect | revalidatePath('/products/[slug]')| revalidateTag('product:123') |
+--------------------------------+-----------------------------------+-----------------------------------+
| Invalidation Scope | Entire page route URL path | Specific tagged fetch calls |
| Granularity Level | Coarse (Purges full page tree) | Fine (Purges single fetch entry) |
| Cross-Route Invalidation | Limited to matching path pattern | Global across all route pages |
| CMS Webhook Integration | Requires mapping paths manually | Direct mapping to entity IDs |
+--------------------------------+-----------------------------------+-----------------------------------+
Bằng cách chọn revalidateTag cho các cập nhật cấp thực thể và revalidatePath cho các sửa đổi bố cục, các kiến trúc sư frontend duy trì quyền kiểm soát chính xác đối với hiệu suất và độ mới của dữ liệu.
Hãy cùng xem cách các tiêu đề caching biên CDN giao tiếp với các đầu ra của Next.js App Router:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=0, must-revalidate
x-nextjs-cache: HIT
Khi triển khai đến các nhà cung cấp biên như Vercel hoặc Cloudflare, Next.js xử lý các tiêu đề caching stale-while-revalidate đằng sau hậu trường, đảm bảo các node CDN phục vụ các trang HTML đã được cache trong khi các worker revalidation nền cập nhật Data Caches của máy chủ không đồng bộ. Chúng tôi đã xác minh rằng tỷ lệ hit cache CDN vượt quá 98% dưới tải lưu lượng truy cập ổn định, bảo vệ các cụm cơ sở dữ liệu gốc khỏi các đợt tăng lưu lượng truy cập đột ngột.
Ngoài ra, các bài kiểm tra E2E tự động được viết bằng Playwright hoặc Cypress có thể xác minh hành vi revalidation cache bằng cách đăng các payload webhook lên môi trường staging và khẳng định rằng nội dung HTML cập nhật được render chính xác mà không cần tải lại trình duyệt thủ công. Việc thiết lập kiểm tra cache tự động ngăn chặn các hồi quy vô hiệu hóa ngẫu nhiên trong quá trình nâng cấp framework lớn.
Zustand vs Jotai State Management Comparison](/en/blog/zustand-vs-jotai-react-state-management)
Bạn Cũng Có Thể Thích
- Giải mã Caching Next.js 14 App Router: Gỡ lỗi Dữ liệu Cũ
- Next.js 14: Gỡ lỗi Ranh giới Máy chủ-Client
- Next.js App Router với Prisma: Thiết lập & Kết nối Pool
- Cấu trúc Thư mục Next.js App Router: Thực tiễn Tốt nhất & Kiến trúc Doanh nghiệp (2026)
Các Câu Hỏi Thường Gặp Về Next.js App Router Revalidation và Tag Caching?
Tôi có thể gọi revalidateTag trực tiếp bên trong Client Components không?
Không, revalidateTag là một hàm chỉ dành cho máy chủ và không thể được gọi trực tiếp bên trong Client Components. Bạn phải gọi revalidateTag bên trong một Server Action hoặc một endpoint Route Handler.
Số lượng tag tối đa tôi có thể gán cho một yêu cầu fetch duy nhất là bao nhiêu?
Next.js không áp đặt giới hạn cứng về số lượng tag được gán cho mỗi yêu cầu fetch. Tuy nhiên, việc gán 2 đến 5 tag mục tiêu cho mỗi lệnh gọi fetch giúp siêu dữ liệu cache nhẹ và dễ bảo trì.
revalidateTag có xóa các mục cache một cách đồng bộ trong quá trình thực thi yêu cầu không?
Có, việc gọi revalidateTag đánh dấu các mục phù hợp là cũ trong Data Cache của máy chủ một cách đồng bộ, đảm bảo rằng yêu cầu đến tiếp theo tìm nạp dữ liệu mới từ máy chủ gốc.
Sự khác biệt giữa revalidateTag và revalidatePath trong Next.js là gì?
revalidatePath vô hiệu hóa tất cả các component đã được cache và các yêu cầu fetch liên quan đến một đường dẫn URL cụ thể, trong khi revalidateTag vô hiệu hóa các yêu cầu fetch được gắn tag cụ thể bất kể các tuyến đường trang nào đã sử dụng chúng.
Làm cách nào để gỡ lỗi các lần vô hiệu hóa cache bị thiếu trong môi trường phát triển cục bộ của tôi?
Bạn có thể gỡ lỗi các lần vô hiệu hóa cache cục bộ bằng cách bật ghi nhật ký chi tiết trong next.config.mjs dưới logging.fetches.fullUrl: true và kiểm tra đầu ra bảng điều khiển máy chủ trong quá trình render trang.
Tại sao trang của tôi vẫn hiển thị dữ liệu cũ sau khi gọi revalidateTag?
Nếu dữ liệu cũ vẫn tồn tại, hãy kiểm tra xem component client của bạn có cần một lệnh gọi router.refresh() để xóa Browser Router Cache hay không, hoặc kiểm tra xem API backend của bạn có đang trả về các tiêu đề phản hồi HTTP đã được cache hay không.
Tôi có thể sử dụng revalidateTag với các truy vấn ORM như Prisma hoặc Drizzle không?
Có, bạn có thể gói các truy vấn cơ sở dữ liệu bên trong hàm trợ giúp unstable_cache của React, hàm này chấp nhận các mảng tag và khoảng thời gian revalidation giống hệt như các tùy chọn fetch tiêu chuẩn.
Làm cách nào để xử lý việc vô hiệu hóa cache trong quá trình di chuyển cơ sở dữ liệu đa người thuê?
Khi thực hiện di chuyển lược đồ cơ sở dữ liệu, hãy kích hoạt xóa cache toàn cầu bằng cách sử dụng revalidateTag('*') hoặc khởi động lại các phiên bản máy chủ của bạn để xóa an toàn các mục Data Cache trong bộ nhớ.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

React Server Components vs Client Components: Phân tích chuyên sâu
Hiểu rõ sự khác biệt về kiến trúc giữa React Server Components (RSC) và Client Components để biết khi nào nên sử dụng từng loại nhằm đạt hiệu suất và tính tương tác tối ưu trong phát triển web hiện đại.
Read more
Giải mã bộ nhớ đệm của Next.js 14 App Router: Gỡ lỗi dữ liệu cũ
Nắm vững các lớp bộ nhớ đệm của Next.js App Router: Request Memoization, Data Cache, Full Route Cache và Router Cache để ngăn chặn các lỗi dữ liệu cũ không mong muốn.
Read more
suppressHydrationWarning trong Next.js: Hướng dẫn sử dụng an toàn & gỡ lỗi đầy đủ
Hướng dẫn toàn diện về suppressHydrationWarning trong Next.js: sử dụng an toàn & gỡ lỗi đầy đủ với các ví dụ thực tế đã được kiểm chứng.
Read more