Caching là chìa khóa để đạt được hiệu suất cao và UX mượt mà. Trong hệ sinh thái React/Next.js hiện đại, caching không chỉ nằm ở một chỗ mà phân tán qua nhiều tầng.
1. Client-Side Caching (Browser & React)
Trước khi request ra khỏi trình duyệt, chúng ta có nhiều lớp phòng thủ.
a. React Memoization (useMemo, useCallback)
Đây là lớp cache ở mức Runtime Memory. Nó không lưu dữ liệu giữa các lần reload trang, nhưng tránh tính toán lại trong 1 phiên.
'use client';
import { useMemo } from 'react';
function ExpensiveComponent({ data }) {
// Cache kết quả xử lý data nặng
// Chỉ tính toán lại khi prop `data` thay đổi
const processedData = useMemo(() => {
return data.map(item => heavyTransformation(item));
}, [data]);
return <List items={processedData} />;
}b. Global State / Client Store
Sử dụng Context API, Redux hoặc Zustand để lưu trữ dữ liệu đã fetch, tránh fetch lại khi chuyển qua lại giữa các component.
c. Data Fetching Cache (React Query / SWR)
Đây là tiêu chuẩn cho Client-side Fetching. Thư viện tự động cache response dựa trên key (URL).
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function UserProfile() {
// SWR tự động cache dữ liệu. Khi component mount lại,
// nó trả ngay cache cũ (stale) rồi ngầm fetch mới (revalidate).
const { data, error } = useSWR('/api/user', fetcher, {
dedupingInterval: 60000, // Reuse cache trong 1 phút
revalidateOnFocus: false // Không fetch lại khi tab focus
});
if (!data) return <div>Loading...</div>;
return <div>Hello {data.name}</div>;
}2. Server-Side Caching (Next.js App Router)
Next.js cung cấp kiến trúc cache mạnh mẽ nhất hiện nay với 4 lớp.
a. Request Memoization (React Server)
Tự động deduplicate các request fetch giống hệt nhau trong cùng một lần render.
Tình huống: Header và MainContent cùng gọi api/user.
Kết quả: API chỉ được gọi 1 lần.
async function getUser() {
// Hàm này có thể được gọi ở nhiều nơi trong tree
const res = await fetch('https://api.example.com/user');
return res.json();
}b. Data Cache (Persistent)
Lưu trữ kết quả fetch qua các lần deployment. Đây là tính năng thay đổi cuộc chơi của Next.js.
// Cache vĩnh viễn (mặc định)
fetch('https://...', { cache: 'force-cache' })
// Cache có thời hạn (ISR) - Chiến lược phổ biến nhất
fetch('https://...', { next: { revalidate: 3600 } }) // 1 giờ3. Các mẫu chiến lược Caching (Caching Patterns)
Chiến lược 1: The "E-commerce Product" (Hybrid Cache)
Yêu cầu: Tải siêu nhanh (TTFB thấp), thông tin giá cả phải tương đối mới. Giải pháp:
- Server: Dùng
ISRvớirevalidate: 60(giây). Trang load nhanh như static, dữ liệu chậm nhất là 60s. - Client: Khi user thêm vào giỏ, dùng Client Fetch (SWR/React Query) để check tồn kho realtime.
Chiến lược 2: The "User Dashboard" (Private & Realtime)
Yêu cầu: Dữ liệu riêng tư, luôn mới nhất, không được cache chung. Giải pháp:
- Server: Dùng
dynamic rendering(cookies(),headers()) hoặccache: 'no-store'. - Client: Data fetching library (SWR/React Query) để polling hoặc realtime updates.
// Server Component - Dashboard
async function Dashboard() {
// Luôn lấy dữ liệu mới nhất
const data = await fetch('api/stats', { cache: 'no-store' });
// ...
}Chiến lược 3: The "Marketing Landing Page" (Full Static)
Yêu cầu: Tốc độ tối đa, chịu tải cao. Giải pháp:
- Server:
cache: 'force-cache'(Mặc định). - Deployment: Build thành static HTML, cache tại CDN (Edge caching).
- Update: Dùng
On-demand Revalidation(Webhook) khi content team sửa nội dung trên CMS.
4. Bảng tổng hợp quyết định (Decision Matrix)
| Yếu tố cần | Chiến lược Cache | Công nghệ |
|---|---|---|
| Dữ liệu tĩnh, ít đổi | Build-time Cache | SSG (Default) |
| Dữ liệu đổi theo giờ/phút | Time-based Revalidation | ISR revalidate: N |
| Dữ liệu realtime (Chat, Stock) | No Cache + Socket | Client-side Fetching |
| CMS Content update bất ngờ | Event-based Revalidation | revalidateTag() |
| Dữ liệu riêng tư (User) | Per-request (No store) | Dynamic Rendering |
5. Kinh nghiệm thực chiến (Senior Insights)
Đừng tin vào revalidate tuyệt đối
Trong hệ thống phân tán (Distributed Systems), khi bạn set revalidate: 60, không có nghĩa là đúng giây thứ 61 cache sẽ bị xóa.
- Thực tế: Request đầu tiên sau 60s sẽ vẫn nhận cache cũ (STALE), đồng thời trigger background revalidation. Chỉ có người tiếp theo mới nhận cache mới.
- Rủi ro: Nếu hệ thống build/deploy lại, Data Cache vẫn tồn tại (Persistent) trừ khi bạn xóa folder
.next/cache. Điều này dễ gây ra lỗi "Code mới nhưng Data cũ".
Cache Stampede (Dog-piling)
Khi một key cache hết hạn và có hàng nghìn request ập vào cùng lúc, server có thể sập vì tất cả đều cố gắng fetch data mới đồng thời.
- Giải pháp: Next.js Request Memoization giúp deduplicate các request trong cùng 1 render, nhưng với nhiều user request đồng thời, bạn cần cơ chế lock ở tầng Infrastructure (Redis) hoặc dùng
stale-while-revalidate(mặc định của Next.js ISR) để giảm tải.
Vấn đề "Over-caching" với Search Params
Mặc định, Full Route Cache sẽ cache trang web. Nhưng nếu bạn dùng searchParams trong Page Component, Next.js sẽ chuyển trang đó sang Dynamic Rendering.
- Lưu ý: Nếu component con cần
searchParams, hãy truyền prop xuống thay vì gọiuseSearchParams()lung tung, để giữ component cha ở trạng thái Static nếu có thể.
6. Câu hỏi phỏng vấn (Interview Q&A)
Q1: Sự khác biệt giữa Request Memoization và Data Cache trong Next.js?
A:
Request Memoizationchỉ sống trong vòng đời của một request duy nhất (để React không fetch trùng lặp khi render component tree).Data Cachesống dai hơn (Persistent), lưu trữ qua các request khác nhau và thậm chí qua các lần deploy, chia sẻ cho mọi user.
Q2: Khi nào nên dùng cache: 'no-store' vs revalidate: 0?
A: Về mặt hành vi chúng tương tự nhau (không cache). Tuy nhiên:
no-store: Ra lệnh cho Data Cache không lưu response này.revalidate: 0: Vẫn có thể lưu nhưng đánh dấu là hết hạn ngay lập tức. Thường dùngno-storerõ ràng hơn cho dữ liệu private/realtime.
Q3: Tại sao dùng fetch trong Server Component tốt hơn gọi DB trực tiếp (Prisma/Mongoose)?
A: Khi dùng
fetch, Next.js tự động kích hoạtData CachevàRequest Memoization. Nếu gọi DB trực tiếp, bạn mất đi cơ chế caching tự động này và phải tự implement cache layer (ví dụ gói hàm DB trongReact.cache()hoặc dùng Redis).
Kết luận
"There are only two hard things in Computer Science: cache invalidation and naming things."
Đừng cache mọi thứ. Hãy bắt đầu với mặc định của Next.js, và chỉ "đục lỗ" cache (opt-out) ở những chỗ cần dữ liệu dynamic. Việc lạm dụng cache client-side (như Redux sai mục đích) thường làm tăng độ phức tạp không cần thiết.