Mục lục

Toàn tập về Caching: React, Next.js & Chiến lược tối ưu

Hướng dẫn chi tiết các loại cache từ Client (React State, Memoization, Browser) đến Server (Next.js 4-layers) và cách lựa chọn chiến lược phù hợp.

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.

tsx:
'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).

tsx:
'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.

tsx:
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.

tsx:
// 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 ISR với revalidate: 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ặc cache: 'no-store'.
  • Client: Data fetching library (SWR/React Query) để polling hoặc realtime updates.
tsx:
// 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ầnChiến lược CacheCông nghệ
Dữ liệu tĩnh, ít đổiBuild-time CacheSSG (Default)
Dữ liệu đổi theo giờ/phútTime-based RevalidationISR revalidate: N
Dữ liệu realtime (Chat, Stock)No Cache + SocketClient-side Fetching
CMS Content update bất ngờEvent-based RevalidationrevalidateTag()
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ọi useSearchParams() 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 MemoizationData Cache trong Next.js?

A: Request Memoization chỉ 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 Cache số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ùng no-store rõ 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ạt Data CacheRequest 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 trong React.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.

Quảng cáo
mdhorizontal