- Định hướng tương lai của Next.js rất thú vị
- Đã có một số vấn đề với Server Actions, nhưng có thể thấy khả năng cải thiện nhờ
useOptimistic và useFormStatus của React 19
- Cách tiếp cận
useFetcher của Remix cũng mang lại DX rất tốt
- PPR (Partial Pre-rendering) và hệ thống cache chi tiết mới của Next.js đặc biệt nổi bật
- Nhìn chung, tôi có ấn tượng rất tích cực
Bức tranh tổng thể
- Có thể bật thử nghiệm hệ thống cache mới trong
next.config.js
- Có thể định nghĩa cache profile để thiết lập các mốc thời gian hết hạn và chu kỳ tái xác thực khác nhau
// next.config.js
const config = {
experimental: {
// Bật hệ thống cache mới. Giờ có thể dùng `use cache` trong code
dynamicIO: true,
// Tùy chọn: thiết lập cache profile
cacheLife: {
blog: {
stale: 3600, // Giữ cache phía client: 1 giờ
revalidate: 900, // Làm mới từ server: 15 phút
expire: 86400, // Tuổi thọ tối đa: 1 ngày
},
},
},
};
Cách dùng cơ bản của use cache
- Có thể cache ở cấp file, component hoặc hàm bằng khai báo
"use cache"
- Trong ví dụ code, chỉ cần thêm
use cache là có thể áp dụng cache dễ dàng
- Có thể dùng
cacheTag, revalidateTag... để vô hiệu hóa cache vào đúng thời điểm mong muốn
// 1. Cache ở cấp file
"use cache";
export default function Page() {
return <div>Cached Page</div>;
}
// 2. Cache ở cấp component
export async function PriceDisplay() {
"use cache";
const price = await fetchPrice();
return <div>${price}</div>;
}
// 3. Cache ở cấp hàm
export async function getData() {
"use cache";
return await db.query();
}
Cache dựa trên tag
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache';
// Cache một nhóm dữ liệu cụ thể
export async function ProductList() {
'use cache';
cacheTag('products');
const products = await fetchProducts();
return <div>{products}</div>;
}
// Vô hiệu hóa cache khi dữ liệu thay đổi
export async function addProduct() {
'use server';
await db.products.add(...);
revalidateTag('products');
}
Cache profile tùy chỉnh
- Có thể dùng
unstable_cacheLife để lấy cache profile đã định nghĩa trong next.config.js
- Áp dụng chính sách cache bằng tên profile được khai báo trong code (ví dụ:
"blog")
import { unstable_cacheLife as cacheLife } from "next/cache";
export async function BlogPosts() {
"use cache";
cacheLife("blog"); // Dùng cache profile blog đã định nghĩa sẵn
return await fetchPosts();
}
Những điểm quan trọng nhưng dễ bị bỏ qua
Tự động tạo cache key
props và arguments của component sẽ tự động được đưa vào cache key
- Các giá trị không thể serialize (như hàm) sẽ được xử lý dưới dạng "tham chiếu không thể sửa đổi"
export async function UserCard({ id, onDelete }) {
"use cache";
// id được đưa vào cache key
// onDelete vẫn được truyền vào nhưng không ảnh hưởng đến cache
const user = await fetchUser(id);
return <div onClick={onDelete}>{user.name}</div>;
}
Trộn nội dung động và nội dung được cache
- Có thể truyền nội dung động vào bên trong nội dung đã cache dưới dạng phần tử con để dùng kết hợp
- Có thể chỉ định mảng
cacheTag để áp dụng và vô hiệu hóa nhiều tag cùng lúc
export async function CachedWrapper({ children }) {
"use cache";
const header = await fetchHeader();
return (
<div>
<h1>{header}</h1>
{children} {/* Nội dung động vẫn được giữ nguyên */}
</div>
);
}
export async function ProductPage({ id }) {
"use cache";
cacheTag(["products", `product-${id}`, "featured"]);
// Có thể vô hiệu hóa bằng bất kỳ tag nào trong số này
}
Cấu trúc phân tầng của cache
- Khi khai báo
"use cache" ở cấp cao nhất, toàn bộ vùng đó sẽ được cache
- Có thể loại trừ một số phần cụ thể (ví dụ: section động dùng Suspense) khỏi vùng cache
"use cache";
export default async function Page() {
return (
<div>
<CachedHeader />
<div>
<Suspense fallback={<Loading />}>
<DynamicFeed /> {/* Nội dung động */}
</Suspense>
</div>
</div>
);
}
Tính an toàn kiểu dữ liệu
- Có thể quản lý các chuỗi như cache key và cache profile dưới dạng hằng số để giảm việc dùng magic string
- Sẽ thuận tiện nếu dùng cách tạo tag theo pattern giống React Query
// Quản lý key của cache profile bằng hằng số
export const CACHE_LIFE_KEYS = {
blog: "blog",
} as const;
const config = {
experimental: {
cacheLife: {
[CACHE_LIFE_KEYS.blog]: {
stale: 3600,
revalidate: 900,
expire: 86400,
},
},
},
};
Cách quản lý tag cache hiệu quả
- Áp dụng tag factory pattern theo kiểu React Query
export const CACHE_TAGS = {
blog: {
all: ["blog"] as const,
list: () => [...CACHE_TAGS.blog.all, "list"] as const,
post: (id: string) => [...CACHE_TAGS.blog.all, "post", id] as const,
comments: (postId: string) =>
[...CACHE_TAGS.blog.all, "post", postId, "comments"] as const,
},
} as const;
// Thiết lập tag cache
function tagCache(tags: string[]) {
cacheTag(...tags);
}
// Ví dụ sử dụng
export async function BlogList() {
"use cache";
tagCache(CACHE_TAGS.blog.list());
}
3 bình luận
Có vẻ chỉ nên dùng các framework như Next.js hoặc Remix khi SEO quan trọng và vì thế cần SSR.
Đặc biệt, tôi nghĩ cần cân nhắc kỹ khi áp dụng Next.js cho các dịch vụ mà SEO không quan trọng, như sản phẩm kinh doanh B2B hay back office. Bởi giao diện mà Next.js áp đặt và độ phức tạp của nó có thể làm giảm năng suất phát triển.
Cá nhân tôi thấy rằng nếu không cần SEO thì Vite + React tốt hơn nhiều về năng suất phát triển và tính linh hoạt.
Tôi thích tương lai của Next.js: nó đang trở nên thật sự rất tuyệt
Từ sau Next.js 13, nó đã trở nên khá dùng được, nhưng dạo gần đây tôi thật sự rất rất thích nó. Có vẻ nó sẽ trở thành tiêu chuẩn thực tế của full-stack web development.