Là một front-end dev và tham gia các dự án sử dụng Next.js trong 3 năm gần đây, tôi có dùng qua nhiều phiên bản, có yêu, có ghét cái framework này. Nhưng để xây dựng các ứng dụng web full-stack hoặc cần tối ưu SEO, tối ưu tốc độ load trang, thì Next.js vẫn là lựa chọn số một trên thị trường.
Cách đây vài ngày, sự kiện Next.js Conf 25 được tổ chức bởi Vercel đã trình làng phiên bản Next.js 16 mới nhất của họ. Theo quan sát của tôi, có một vài điểm thay đổi đáng chú ý, có thể làm thay đổi cách tiếp cận tối ưu ứng dụng web trong tương lai.
Cache Components
Tính năng cache của Next.js trước phiên bản 16 chủ yếu dựa trên 2 phương án caching:
- Full route cache (cache toàn trang): Cache lại kết quả render của cả một trang. Tốc độ phục vụ rất nhanh, nhưng nhược điểm là nội dung hiển thị sẽ bị cũ nếu không được revalidate (làm mới cache). Với phương án này, chúng ta đang trói buộc tất cả thành phần trên một trang phải cùng tĩnh, hoặc cùng động, chưa thể hỗn hợp cả hai.
- Request memoization: Đối với những trang động (phải tìm nạp và hiển thị dữ liệu ứng với mỗi request), Next.js hỗ trợ cache lại kết quả trả về của
fetchAPI, giúp giảm thiểu thời gian chờ đợi tìm nạp dữ liệu từ một nguồn nào đó dựa trên HTTP request. Nhưng ngoàifetchAPI ra, Next.js không hỗ trợ cache cho các hành động server-side IO khác, ví dụ như: gửi HTTP request bằng Axios, query DB bằng ORM (Prisma, Drizzle). Nếu muốn áp dụng khả năng cache cho những hành động này, chúng ta sẽ dùng hàmunstable_cachecủa Next.js, hoặc dùng hàmcachecủa React. Tôi không chuộng dùng chúng lắm, vì các dự án tôi làm đều dùngfetchđể gọi sang một backend riêng, nên tôi xin phép không bàn sâu hơn.
Nhưng từ phiên bản Next.js 16, chiến lược cache đã trở nên tổng quát hơn. Cache Components là một tính năng mới, giúp đưa caching trở thành khái niệm gốc trong mô hình component của Next.js, không còn bị giới hạn ở mức route hoặc fetch.
Ví dụ áp dụng Cache Components cho server component:
export default async function Page() {
return (
<>
<StandingsTable />
<Suspense>
<NewsStories />
</Suspense>
</>
)
}
async function StandingsTable() {
"use cache"
await fetch('...')
// ...
}
async function NewsStories() {
await fetch('...')
// ...
}
Khi reload trang, thành phần StandingsTable sẽ hiển thị ngay tức thì do đã được cache, chỉ có thành phần NewsStories render lại mỗi khi reload trang.
Mặc định tất cả component đều là động. Directive mới "use cache" dùng để đánh dấu một component hoặc một hàm rằng Next.js sẽ cache chúng.
Khi đã dùng directive "use cache", chúng ta nên kết hợp thêm dùng hàm cacheLife để thiết lập thời gian sống của cache, bởi nếu chỉ dùng directive không thôi thì cache sẽ tồn tại vô thời hạn. Next.js cung cấp thêm các hàm mới như cacheTag để đánh dấu tag cho hàm được cache, updateTag để làm mới data ngay lập tức, revalidateTag để đánh dấu một tag là đã cũ và sẽ làm mới data trong request tiếp theo.
Ví dụ áp dụng Cache Components cho server action:
"use server"
export async function getUsers() {
"use cache"
cacheLife("minutes")
cacheTag("users")
const users = await prisma.user.findMany(...)
return users
}
export async function getUserById(id: string) {
"use cache"
cacheLife("minutes")
cacheTag(`users:${id}`)
const users = await prisma.user.findUnique({ where: { id } })
return users
}
Lưu ý khi sử dụng "use cache":
- Chỉ áp dụng cho các hàm hoạt động ở phía server
- Chỉ được áp dụng cache với các phương thức get dữ liệu, không áp dụng với các phương thức làm thay đổi dữ liệu (createUser, updateUser, …)
- Các đối số của hàm phải là serializable (nhưng có thể chấp nhận đối số unserializable nếu chỉ pass qua mà không đọc chúng, ví dụ
childrencủa một component)
Còn nhiều điều mới mẻ về tính năng Cache Components mà tôi chưa tìm hiểu kĩ, ví dụ như:
- Khi component cần đọc tham số từ params, searchParams, cookies, … thì dùng cache thế nào?
- Quy tắc đặt tên tag, flow cache và làm mới cache như thế nào sao cho clean?
Tôi xin khảo cứu ở một bài viết khác.
middleware –> proxy
Middleware trong Next.js có chức năng biến đổi request hoặc response, điều hướng request (redirect) hay làm biến đổi đường dẫn (rewrite) trước khi đi tới router của ứng dụng. Use case phổ biến nhất đó là chúng ta hay dùng middleware để xử lý authentication.
Từ Next.js 16, middleware được khuyến khích đổi tên thành proxy. Chức năng của nó vẫn như cũ, chỉ là đổi tên thôi, nhưng ý nghĩa đằng sau đó là gì?
Next.js có nhiều bối cảnh thực thi
Trong các ứng dụng thuần backend, ứng dụng thường là một instance duy nhất và chạy trong một bối cảnh thực thi (execution context) duy nhất. Do đó middleware ở cấp độ application-level có thể wrap chung cho tất cả các route của ứng dụng đó. Các ứng dụng thuần frontend cũng vậy.
Nhưng Next.js lại có nhiều bối cảnh thực thi:
- Server components: Node runtime
- Server actions: Node runtime
- Route handlers: Node runtime
- Client components: Browser runtime
- Middleware: Edge runtime
Middleware của Next.js không giống như middleware của Express
Trong Express, một ứng dụng có thể có nhiều middleware, lồng nhau tạo thành một chuỗi xử lý nối tiếp, ở cấp độ application-level hay route-level. Next.js thì không như vậy. Nó chỉ có một hàm middleware duy nhất. Bạn muốn xử lý logic chung cho một nhóm route nào đó? Bạn có thể dùng Layout để làm điều đó.
Server action không đi qua middleware
Server action được gọi trực tiếp vào trong server component chứ không được gọi qua HTTP request như page và route handler. Do đó server action không đi qua middleware. Bạn có thể gọi server action trong một client component, nhưng về bản chất là bạn đang thực hiện một POST request tới page và page gọi server action hộ bạn.
Không nên xử lý authorization ở middleware
Mục đích của middleware là xử lý nhanh các request, chặn hoặc điều hướng request tới các route. Nếu bắt request đợi để xác minh authorization (mất thời gian query DB, hoặc gọi API bên ngoài) thì không hợp lý. Ngoài ra gần đây đã có những lỗi vulnerable rất nghiêm trọng liên quan đến việc middleware của Next.js bị bypass. Vì thế nhiều developer khuyến nghị chúng ta nên xử lý authorization ở từng layout, page và từng server action.
Với tính năng cache mới ở trên thì tôi sẽ dùng Axios thay cho fetch để gọi API, tạo một Axios instance, xử lý authorization tại interceptor, rồi cứ thế dùng instance đó để gọi API trong các server action.
Tóm lại, Next.js muốn đổi tên từ “middleware” sang “proxy” để tránh gây nhầm lẫn với khái niệm “middleware” của các framework khác. Và cũng qua đó, chúng ta thấy rằng mỗi page, mỗi server action cần phải tự bảo vệ chính nó, không nên ủy nhiệm chức năng authorization cho proxy.
Nhận xét và dự đoán
Dường như mỗi component đang trở thành “đơn vị tiền tệ” mới trong một ứng dụng hybrid. Các xử lý như rendering, caching, server IO (data fetching, DB query) trước đây thuộc về cấp độ application-level đang dần hạ xuống cấp độ component-level, function-level.
Liệu rằng trong tương lai, mỗi component có thể được triển khai độc lập, scale độc lập và được phát tán trên các nền tảng serverless? “Đám mây” components, rất thú vị!


