Có một câu hỏi quay đi quay lại trong gần như mọi buổi review code React: "chỗ này có cần bọc useMemo không?". Rồi React Compiler lên 1.0 (tháng 10/2025), và tới Next.js 16 thì reactCompiler được promote từ experimental thành option chính thức. Nghe như câu trả lời cuối cùng cho nỗi đau memo hoá bằng tay.
Thực tế thì compiler không xoá sạch useMemo, và nó cũng không miễn phí. Dưới đây là những gì mình rút ra sau khi bật nó cho vài dự án Next.js.
Bật compiler là một dòng, nhưng không phải một dòng thần kỳ
Trước hết vẫn phải cài plugin, Next không tự kéo về:
npm install -D babel-plugin-react-compiler@latest
Rồi bật trong config. Ở Next.js 16 không còn experimental nữa:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;
Điều đáng chú ý: Next bật sẵn option này nhưng không bật mặc định. Lý do họ nói thẳng trong release note — compiler chạy qua Babel, nên thời gian dev và build sẽ tăng. Với dự án nhỏ thì không thấy gì, với repo vài nghìn component thì next build chậm hơn rõ rệt.
Hướng đi đang tốt lên: đội Oxc làm oxc-transform-react (biến thể Rust của compiler) và benchmark sơ bộ nhanh hơn Babel khoảng 10 lần; @vitejs/plugin-react từ v6.1 đã dùng được. Còn Next thì đang phối hợp với đội swc để có đường build nhanh hơn. Nói ngắn gọn: đừng lấy lý do "build chậm" để trì hoãn mãi, nhưng cũng đừng bật all-in rồi ngồi nhìn CI tăng gấp rưỡi.
Nên bật từng phần trước khi bật hết
Cách mình thích nhất là compilationMode: "annotation". Compiler sẽ nằm im, chỉ compile những component bạn đánh dấu bằng directive "use memo":
// next.config.ts
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: "annotation",
},
};
"use memo";
type Item = { id: string; name: string; price: number };
export function ProductList({ items, keyword }: { items: Item[]; keyword: string }) {
const filtered = items.filter((i) => i.name.includes(keyword));
const total = filtered.reduce((sum, i) => sum + i.price, 0);
return (
<ul>
{filtered.map((i) => (
<li key={i.id}>
{i.name} — {i.price}
</li>
))}
<li>Tổng: {total}</li>
</ul>
);
}
Không còn useMemo nào ở đây, mà component vẫn được memo hoá theo đúng phụ thuộc. Bật từng file theo directive giúp mình thấy được file nào compiler "chê", file nào chạy ổn — thay vì bật hết rồi truy một bug trong im lặng.
Cái bẫy thật: thư viện mutate nội bộ
Compiler dựa trên giả định component là hàm thuần, tham số không bị sửa. Vài thư viện phổ biến thì làm ngược lại: watch() của React Hook Form, TanStack Table, TanStack Virtual, MobX, một số thư viện chart/canvas. Với chúng, compiler hoặc bỏ qua component, hoặc tệ hơn là memo hoá sai chỗ khiến UI không cập nhật.
Cách xử lý là opt-out tại chỗ bằng "use no memo":
function FilterForm() {
"use no memo";
const { register, watch } = useForm<{ q: string }>();
const q = watch("q"); // giá trị đổi theo cách compiler không theo dõi được
return <input {...register("q")} placeholder={q} />;
}
Việc bắt buộc phải làm trước đó: nâng eslint-plugin-react-hooks lên v7+ và bật preset recommended. Plugin này giờ mang luôn các rule của compiler — purity, refs, immutability, preserve-manual-memoization, set-state-in-effect, incompatible-library. Chạy lint trước khi bật compiler là bước rủi ro thấp nhất mà lại dọn được nhiều thứ nhất.
Đừng tin badge ✨ trong DevTools
Bật compiler rồi, mở React DevTools thấy chữ "Memo ✨" cạnh tên component — nhiều người tưởng thế là xong. Badge đó chỉ xác nhận compiler đã chạy qua file, không nói gì về việc app nhanh hơn.
Muốn biết thật, dùng Profiler đo số lần render trước/sau, trên cùng một luồng thao tác. Và nhớ: compiler chỉ dọn phần "memo hoá thừa". Nếu state đặt sai chỗ khiến cả cây component render lại, hay context value bị tạo mới mỗi render, thì compiler không cứu được — đó vẫn là bug kiến trúc.
Vậy còn giữ useMemo khi nào?
Ba trường hợp mình vẫn giữ tay:
- Phép tính thật đắt (parse dữ liệu lớn, build index) mà bạn muốn kiểm soát rõ ràng thay vì tin compiler.
- Giá trị/đối tượng được truyền vào thư viện ngoài như một "hợp đồng" về identity — nơi việc memo hay không là một phần API, không phải chi tiết tối ưu.
- Component đã
"use no memo"vì thư viện nội bộ mutate, khi đóuseMemovẫn là công cụ duy nhất.
Ngoài ba trường hợp đó thì mặc định nên xoá. Code React sạch hơn, review bớt cãi nhau về dependency array, và compiler làm phần việc lặp lại.
Điều mình thấy đáng nói nhất: React Compiler không phải "tính năng tăng tốc" như marketing hay kể, nó là một hợp đồng — muốn dùng thì code của bạn phải đúng chuẩn. Repo nào còn nhiều chỗ vi phạm Rules of React thì bật compiler sẽ phơi hết ra, và đó lại là tin tốt.
📋 Phụ lục thuật ngữ
| Thuật ngữ | Giải thích ngắn |
|---|---|
| React Compiler | Trình biên dịch của React, tự thêm memo hoá vào component/hook lúc build, không cần useMemo/useCallback thủ công |
compilationMode: "annotation" |
Chế độ bật từng phần: chỉ file có directive "use memo" mới được compile |
"use no memo" |
Directive opt-out cho một component/hàm cụ thể |
| Interior mutability | Thư viện trả về object/hàm bị sửa nội bộ sau khi tạo — compiler không memo hoá an toàn được |
| Rules of React | Bộ quy tắc (thuần, không sửa tham số, không đọc ref khi render) compiler dựa vào để tối ưu |
oxc-transform-react |
Bản compiler chạy bằng Rust của Oxc, nhanh hơn babel-plugin-react-compiler khoảng 10 lần |
| Memo ✨ badge | Dấu trong React DevTools báo component đã qua compiler — chỉ là kiểm tra kết nối, không phải bằng chứng hiệu năng |
