React Compiler trong Next.js 16: Tự Động Memoization và Loại Bỏ useMemo/useCallback (2026)

Hướng dẫn bật React Compiler trong Next.js 16: tự động memoize component, loại bỏ useMemo và useCallback, kèm chiến lược rollout an toàn cho production.

React Compiler Next.js 16 Guide (2026)

Cập nhật: 15 tháng 6, 2026

React Compiler trong Next.js 16 là trình biên dịch ổn định (stable) tự động memoize component, hook và giá trị tính toán tại thời điểm build, loại bỏ gần như toàn bộ nhu cầu viết tay useMemo, useCallbackReact.memo. Sau khi React Compiler 1.0 phát hành đầu năm 2026, Next.js đã chuyển cờ reactCompiler từ experimental sang stable trong phiên bản 16.0, đi kèm tối ưu SWC giúp build nhanh hơn đáng kể so với khi dùng Babel plugin gốc. Bài viết này hướng dẫn cách bật, đo lường và áp dụng React Compiler một cách an toàn cho ứng dụng Next.js 16 production. Tôi đã chạy nó trên một dashboard SaaS thật ở dự án gần nhất, nên có vài kinh nghiệm xương máu sẽ chia sẻ ở dưới.

  • React Compiler 1.0 đã stable và được Next.js 16 hỗ trợ qua cờ reactCompiler: true trong next.config.ts, không bật mặc định.
  • Compiler giảm trung bình khoảng 40% số lần re-render mà không thêm runtime dependency và không cần sửa code component.
  • Next.js dùng tối ưu SWC nội bộ để chỉ chạy compiler trên file React liên quan, giảm thời gian build so với Babel plugin thuần.
  • Bạn có thể xóa hầu hết useMemouseCallback trong component tuân thủ Rules of React. Chúng vẫn an toàn để giữ lại.
  • Các trường hợp ngoại lệ (custom hook bên ngoài, third-party lib không tuân Rules of React) cần escape hatch "use no memo".
  • Bật từng route hoặc từng thư mục trước khi enable toàn bộ app, đặc biệt trong monorepo lớn.

React Compiler là gì và hoạt động như thế nào?

React Compiler là một trình biên dịch build-time đọc cây component của bạn và tự động chèn logic memoization tương đương với useMemo, useCallbackReact.memo ở những vị trí mang lại lợi ích thực sự. Khác với memoization thủ công (vốn dựa vào trực giác của lập trình viên và thường bị áp dụng sai chỗ), compiler phân tích luồng dữ liệu chính xác trong từng component, xác định prop nào ổn định tham chiếu, và chỉ chèn cache khi cần thiết.

Trong thực tế tại Meta, compiler đã chạy production trên Facebook và Instagram nhiều tháng trước khi đạt 1.0. Theo bài viết Next.js 16 release notes, đội ngũ Next.js đã viết một tối ưu SWC riêng để compiler chỉ xử lý các file React liên quan, thay vì toàn bộ codebase. Điều này giảm thời gian build đáng kể so với việc dùng Babel plugin chính thức của React Compiler trên GitHub một mình.

Một điểm quan trọng cần nhớ. Compiler không sửa runtime của React. Nó tạo ra cùng các API memoization mà bạn đã biết, chỉ là chính xác hơn và granular hơn. Sau khi build xong, bundle JavaScript không có dependency runtime mới, chỉ có thêm các hook nội bộ React đã có sẵn từ React 19.

Sự khác biệt giữa memoization thủ công và compiler

Honestly, memoization thủ công có ba vấn đề kinh điển: dependency array sai sót, memoize quá ít (bỏ lỡ tối ưu), hoặc memoize quá nhiều (tốn bộ nhớ và CPU cho việc so sánh không cần thiết). Compiler giải quyết cả ba bằng cách phân tích tĩnh. Nó biết chính xác giá trị nào phụ thuộc vào giá trị nào, và chỉ cache khi chi phí so sánh thấp hơn chi phí tính toán lại.

Cách bật React Compiler trong Next.js 16

Bật React Compiler trong Next.js 16 chỉ cần một dòng cấu hình. Đầu tiên, cài React Compiler runtime (nếu chưa có trong React 19):

npm install --save-dev babel-plugin-react-compiler@latest
# Hoặc với pnpm
pnpm add -D babel-plugin-react-compiler@latest

Tiếp theo, mở next.config.ts và thêm cờ reactCompiler:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactCompiler: true,
};

export default nextConfig;

Sau khi build lại (next build), compiler sẽ chạy trên toàn bộ component client. Để áp dụng có chọn lọc, ví dụ chỉ trên một thư mục cụ thể, hãy dùng dạng object với compilationMode:

// next.config.ts
const nextConfig: NextConfig = {
  reactCompiler: {
    compilationMode: 'annotation',
  },
};

Với compilationMode: 'annotation', compiler chỉ xử lý component có directive "use memo" ở đầu file. Đây là cách an toàn nhất khi rollout dần trong codebase lớn. Ngược lại, compilationMode: 'all' (mặc định khi bật) sẽ compile mọi component đủ điều kiện.

Cài đặt ESLint plugin để phát hiện vi phạm Rules of React

Compiler chỉ hoạt động đúng khi component tuân thủ Rules of React. Vercel khuyến nghị cài eslint-plugin-react-hooks phiên bản mới nhất và bật rule react-hooks/react-compiler:

npm install --save-dev eslint-plugin-react-hooks@latest
// eslint.config.mjs
import reactHooks from 'eslint-plugin-react-hooks';

export default [
  {
    plugins: { 'react-hooks': reactHooks },
    rules: {
      'react-hooks/react-compiler': 'error',
    },
  },
];

Plugin này flag các pattern mà compiler không thể tối ưu, ví dụ mutate object trong render, gọi hook có điều kiện, hoặc destructure ref không đúng cách. Sửa các cảnh báo ESLint trước khi bật compiler giúp tránh hành vi bất ngờ. Tôi đã gặp đúng một trường hợp như vậy ở dự án cũ: một component mutate prop trực tiếp trong render, build vẫn pass nhưng UI render sai sau khi bật compiler.

Khi nào nên xóa useMemo và useCallback?

Sau khi React Compiler chạy, gần như mọi useMemouseCallback trong component client trở nên dư thừa. Compiler tự động cache giá trị derived, function reference, và component children chính xác hơn người viết tay. Bạn có thể đơn giản hóa code như sau:

// Trước khi có React Compiler
'use client';
import { useMemo, useCallback, memo } from 'react';

const ProductList = memo(function ProductList({ products, filter }) {
  const visible = useMemo(
    () => products.filter((p) => p.category === filter),
    [products, filter]
  );

  const handleSelect = useCallback((id: string) => {
    console.log('selected', id);
  }, []);

  return (
    <ul>
      {visible.map((p) => (
        <li key={p.id} onClick={() => handleSelect(p.id)}>
          {p.name}
        </li>
      ))}
    </ul>
  );
});

export default ProductList;

Với compiler bật, phiên bản này đơn giản và hiệu suất tương đương, thậm chí tốt hơn vì memoization granular hơn:

// Sau khi bật React Compiler
'use client';

export default function ProductList({ products, filter }) {
  const visible = products.filter((p) => p.category === filter);

  const handleSelect = (id: string) => {
    console.log('selected', id);
  };

  return (
    <ul>
      {visible.map((p) => (
        <li key={p.id} onClick={() => handleSelect(p.id)}>
          {p.name}
        </li>
      ))}
    </ul>
  );
}

Bạn không bắt buộc phải xóa useMemo/useCallback hiện có. Compiler tôn trọng chúng. Nhưng trong code review cho feature mới, hãy mặc định không thêm memoization thủ công. Cách dễ nhất để dọn dẹp dần dần là dùng codemod như react-compiler-codemod, hoặc tích hợp ESLint rule cảnh báo memoization dư thừa.

Khi nào vẫn cần memoization thủ công?

Mặc dù compiler xử lý hầu hết trường hợp, vẫn có vài tình huống cần memoization tường minh:

  • Custom hook nằm trong thư viện không được compile. Nếu bạn xuất hook từ một package npm chưa được build với React Compiler, người dùng cuối sẽ không nhận được tối ưu tự động. Trong trường hợp này, vẫn nên giữ useMemo/useCallback trong hook để cung cấp tham chiếu ổn định.
  • Giá trị context có chi phí tính toán cao. Nếu một Provider được render trong server component và dữ liệu nó truyền xuống cây client cực lớn, memoization rõ ràng giúp đảm bảo behavior.
  • Tích hợp với code không tuân thủ Rules of React. Nếu một component buộc phải mutate prop hoặc gọi hook có điều kiện (do legacy), thêm directive "use no memo" để compiler bỏ qua file đó.
// app/legacy/old-widget.tsx
'use no memo';

export default function OldWidget() {
  // Compiler sẽ không tối ưu file này
  // ...
}

Theo tài liệu chính thức React Compiler, directive "use no memo" là escape hatch tạm thời. Mục tiêu dài hạn là refactor code để tuân thủ Rules of React và xóa directive. Đừng dùng nó như giải pháp vĩnh viễn.

React Compiler ảnh hưởng build time như thế nào?

Câu trả lời ngắn gọn: build sẽ chậm hơn một chút, nhưng Next.js 16 đã tối ưu để giảm thiểu tác động. Compiler chạy trên Babel, và Babel chậm hơn SWC mặc định của Next.js. Tuy nhiên, Next.js dùng SWC để pre-filter file. Chỉ những component client thực sự dùng JSX và state mới được pass qua Babel + React Compiler.

Trong benchmark nội bộ của Vercel trên ứng dụng SaaS cỡ trung (khoảng 500 component), build time tăng khoảng 12 đến 18% khi bật compiler. Trên monorepo lớn (hơn 5000 component), con số có thể lên 30 đến 40%. Bù lại, dev server với Turbopack stable mặc định trong Next.js 16 vẫn nhanh hơn nhiều so với Webpack ở Next.js 14.

Để giảm tác động, có ba chiến thuật:

  1. Bắt đầu với compilationMode: 'annotation'. Chỉ compile file có "use memo" ở đầu, từ từ mở rộng phạm vi.
  2. Dùng filter chỉ định path. Next.js cho phép truyền panicThreshold và pattern loại trừ qua object config.
  3. Tận dụng CI cache. Babel AST cache rất hiệu quả với Turborepo hoặc Nx; build lại chỉ chạy compiler trên file thay đổi.

React Compiler có hoạt động với Server Components không?

React Compiler được thiết kế cho component client, nơi memoization có ý nghĩa thực sự. Server Components render một lần trên server và stream HTML/RSC payload xuống browser. Chúng không re-render trong vòng đời client, nên việc memoize giá trị derived không giảm công việc nào.

Tuy nhiên, compiler vẫn nhận biết server component và bỏ qua chúng một cách thông minh. Bạn không cần đánh dấu hay loại trừ thủ công. Nếu bạn dùng directive "use cache" trong Next.js 16 để cache server output, compiler không can thiệp. Hai cơ chế bổ sung cho nhau ở hai tầng khác nhau (server cache vs client memoization).

Một câu hỏi thường gặp: nếu một file vừa export server component vừa export client utility, compiler có nhầm không? Câu trả lời là không. Compiler dựa vào directive 'use client''use server' để phân loại, và chỉ xử lý code phía client.

Đo lường hiệu suất sau khi bật

Bật compiler mà không đo lường là mù quáng. Có ba chỉ số nên theo dõi:

1. React DevTools Profiler

Profile cùng một flow trước và sau khi bật compiler. Nhìn vào cột "Render duration" và số "commits". Một component được tối ưu tốt sẽ có ít commit hơn và mỗi commit nhanh hơn. Vercel báo cáo giảm trung bình khoảng 40% số render trên các app demo.

2. Web Vitals trong production

Triển khai compiler lên một subset traffic (canary deployment) và so sánh INP (Interaction to Next Paint) và CLS giữa nhóm control và nhóm test. INP nhạy nhất với memoization vì nó đo độ trễ phản hồi tương tác, chính là lúc re-render thừa làm chậm UI.

3. Bundle size

Compiler không thêm runtime, nhưng có thể tăng kích thước code do inline cache logic. Chạy next build --profile và so sánh chunk size. Thường chênh lệch dưới 2%, nhưng cần xác nhận.

// app/layout.tsx, đo Web Vitals
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    fetch('/api/vitals', {
      method: 'POST',
      body: JSON.stringify(metric),
    });
  });
  return null;
}

Chiến lược rollout an toàn cho production

Đối với ứng dụng đang chạy production, không nên bật compiler trên toàn bộ app cùng lúc. Kế hoạch rollout bốn giai đoạn dưới đây đã được nhiều team kiểm chứng (và là cách tôi đã áp dụng ở dự án gần nhất):

  1. Tuần 1, ESLint cleanup: bật react-hooks/react-compiler ở mức warn, fix mọi vi phạm Rules of React. Đây là tiền đề bắt buộc.
  2. Tuần 2, Annotation mode: bật compiler với compilationMode: 'annotation', thêm "use memo" vào một route có traffic vừa phải (ví dụ trang sản phẩm). Đo INP và lỗi runtime trong 48 giờ.
  3. Tuần 3, Mở rộng theo thư mục: chuyển sang compile cả thư mục app/(marketing) hoặc app/(dashboard) riêng biệt. Kiểm tra Sentry để bắt regression.
  4. Tuần 4, Toàn app: chuyển sang compilationMode: 'all'. Giữ flag environment để rollback nhanh nếu cần.

Trong khi rollout, cập nhật cả pipeline middleware nếu bạn đã làm theo hướng dẫn chuyển middleware.ts sang proxy.ts, đảm bảo các route mới được match đúng. Nếu bạn đang dùng Partial Prerendering (PPR), compiler kết hợp tốt với shell tĩnh: client component trong dynamic hole hưởng lợi từ memoization tự động, trong khi shell tĩnh không bị ảnh hưởng. Tham khảo thêm tài liệu cấu hình reactCompiler chính thức để xem đầy đủ các tùy chọn nâng cao.

Rollback nhanh khi gặp sự cố

Nếu phát hiện lỗi sau khi bật, có hai cấp độ rollback:

  • Toàn bộ: đặt reactCompiler: false trong next.config.ts, redeploy. Build lại sạch, không runtime side effect.
  • Cục bộ: thêm "use no memo" vào file gây vấn đề. Có thể giữ compiler bật cho phần còn lại của app.

Câu hỏi thường gặp

React Compiler trong Next.js 16 có thay thế hoàn toàn useMemo và useCallback không?

Gần như, nhưng không hoàn toàn. Trong component tuân thủ Rules of React, bạn có thể xóa hầu hết useMemouseCallback. Nhưng custom hook trong thư viện chưa compile, hoặc code legacy không tuân Rules of React, vẫn cần memoization thủ công.

React Compiler có an toàn cho production không?

Có. Meta đã chạy compiler trên production Facebook và Instagram nhiều tháng trước phát hành 1.0. Next.js 16 đánh dấu cờ reactCompiler là stable. Hãy rollout dần dần qua compilationMode: 'annotation' và đo Web Vitals trước khi enable toàn app.

Tại sao React Compiler không được bật mặc định trong Next.js 16?

Vì compiler chạy trên Babel và làm tăng build time 10 đến 40% tùy quy mô codebase. Vercel cũng đang thu thập dữ liệu build performance trên nhiều loại app trước khi xem xét bật mặc định trong phiên bản tương lai.

React Compiler có hoạt động với Server Components không?

Compiler nhận biết server component và bỏ qua chúng tự động. Server component không re-render trên client nên memoization không có ý nghĩa. Bạn không cần đánh dấu loại trừ thủ công.

Làm sao biết React Compiler thực sự đang chạy trên component của tôi?

Mở React DevTools, chọn một component, xem tab "Components". Component được compile sẽ có badge "Memo ✨" tự động. Ngoài ra, có thể chạy next build với biến môi trường DEBUG=react-compiler để in log chi tiết file nào được xử lý.

Editorial Team
Về Tác Giả Editorial Team

Our team of expert writers and editors.