Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

React 19 Compiler: Auto-Memoization và Actions Hooks giúp tăng performance cho web app

Ếch Trendy
Ếch Trendy

5 tháng trước · 10 phút đọc

React Compiler là gì và tại sao bạn nên quan tâm ngay bây giờ

Bạn có bao giờ nhìn vào một component React và thấy mình đang viết useMemo, useCallback, React.memo nhiều hơn cả logic thực sự không? Mình từng có một file component dài 300 dòng mà gần 80 dòng là các wrapper memo. Không phải vì app phức tạp - mà vì mình sợ re-render.

React Compiler (trước đây có tên React Forget) ra mắt stable với React 19 giải quyết đúng vấn đề đó. Thay vì bạn phải tự identify và annotate từng chỗ cần memoize, compiler phân tích toàn bộ codebase tại build-time, tự động inject memoization ở đúng chỗ cần thiết.

343555 Compiler làm việc ở build-time - bạn viết code bình thường, nó lo phần optimize

Nó được release stable từ tháng 10/2025 dưới dạng Babel plugin babel-plugin-react-compiler, là opt-in (không bật mặc định). Bạn vẫn giữ toàn quyền kiểm soát.

Điểm khác biệt quan trọng so với memoization thủ công: compiler hoạt động ở granular level, tức là prop-level thay vì toàn bộ component. Nó chỉ skip re-render cho phần nào thực sự không thay đổi, không phải block cả component như React.memo truyền thống.


Cơ chế hoạt động: 3 phase build-time analysis

Compiler không phải magic - nó chạy 3 bước phân tích tại build-time trước khi bundle code của bạn.

Phase 1 - Analysis: Parse AST (cây cú pháp), theo dõi data-flow và mutability của từng biến, prop, state.

Phase 2 - Inference: Phân loại values thành reactive (props/state thay đổi theo user interaction) và static (hằng số, pure functions không phụ thuộc state).

Phase 3 - Transformation: Inject memoization boundaries tại đúng vị trí. Không memoize những gì rẻ để tính toán lại.

343556 3 phase phân tích - memoization chỉ được inject khi thật sự cần thiết

Một ví dụ thực tế. Bạn có component này:

// Code bạn viết - không có memo nào
const UserCard = ({ user, theme }) => (
  <div className={`card card--${theme}`}>
    <h3>{user.name}</h3>
    <p>{user.email}</p>
  </div>
);

Compiler sẽ phân tích và nhận ra themeuser là props reactive. Nó sẽ inject equality checks ở prop-level - nếu user.name không đổi nhưng parent re-render vì lý do khác, component này sẽ skip.

Một điểm mình thấy thú vị hơn cả: conditional memoization. Manual React.memo không hoạt động tốt với early returns (vì nó wrap toàn bộ component output). Compiler thì xử lý được - memoize ngay cả khi component có early return ở giữa.

// Compiler xử lý được case này - manual memo thì không
const ProductCard = ({ product, isLoading }) => {
  // Early return
  if (isLoading) return <Skeleton />;

  // Phần này vẫn được memoize riêng
  return (
    <div>
      <h3>{product.name}</h3>
      <Price value={product.price} />
    </div>
  );
};

Nhưng có những pattern sẽ block compiler - không auto-memo được:

  • Mutate props hoặc refs trong render
  • Dùng Math.random(), Date.now() trực tiếp trong render
  • Close over mutable variables từ bên ngoài component

Nhớ nguyên tắc này: nếu component của bạn không pure thì compiler sẽ skip nó. Đây thật ra là lý do tốt để refactor code cũ.


Actions hooks trong React 19: useActionState, useFormStatus, useOptimistic

Bên cạnh auto-memoization, React 19 giới thiệu một nhóm hooks mới gọi là Actions - thay đổi cách bạn xử lý forms và async mutations, đặc biệt khi kết hợp Next.js 15 Server Actions.

Vấn đề với React 18 và forms: bạn cần tự manage loading state, error state, optimistic updates - mỗi thứ một useState, một useEffect, và cả đống re-renders không cần thiết.

343557 Actions hooks gom logic form vào 1 chỗ - code ít hơn, re-render ít hơn

useActionState là hook đáng dùng nhất trong bộ này:

import { useActionState } from 'react';

// Server action (Next.js 15)
async function submitOrder(prevState, formData) {
  const result = await createOrder(formData.get('productId'));
  return result.success
    ? { status: 'success', message: 'Đặt hàng thành công!' }
    : { status: 'error', message: result.error };
}

// Component - sạch hơn rất nhiều
function OrderForm({ productId }) {
  const [state, action, isPending] = useActionState(submitOrder, null);

  return (
    <form action={action}>
      <input type="hidden" name="productId" value={productId} />
      <button disabled={isPending}>
        {isPending ? 'Đang xử lý...' : 'Đặt hàng'}
      </button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

So sánh với cách React 18: bạn sẽ cần ít nhất 3-4 useState để làm điều tương tự, cộng thêm một useCallback để wrap handler, cộng thêm potential re-render bugs.

useOptimistic giải quyết UX cho trường hợp bạn cần update UI ngay lập tức mà không chờ server:

function CommentList({ comments, addComment }) {
  // Hiện comment ngay khi user submit, revert nếu server lỗi
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment) => [...state, { ...newComment, isPending: true }]
  );

  return (
    <ul>
      {optimisticComments.map(comment => (
        <li key={comment.id} style={{ opacity: comment.isPending ? 0.6 : 1 }}>
          {comment.text}
        </li>
      ))}
    </ul>
  );
}

Kết hợp compiler + actions hooks, re-renders trong form flow giảm đáng kể. Compiler tự memo các phần static của UI, actions hooks gom async logic vào một chỗ có internal batching.


Benchmark thực tế: compiler vs manual useMemo

Claim "tăng performance 3x" xuất hiện nhiều trong các bài marketing. Thực tế thì phức tạp hơn - số này phụ thuộc rất nhiều vào loại app và pattern cũ của bạn.

Dựa trên các benchmark được ghi nhận, list apps và table với cascading re-renders là nơi compiler thể hiện rõ nhất: 60% reduction in re-render count (dev.to React Compiler Deep Dive).

343558 Benchmark phụ thuộc heavily vào app - đừng tin số marketing, tự đo với DevTools

Mình tổng hợp so sánh thực dụng:

Tiêu chí Manual useMemo/React.memo React Compiler
Identify điểm cần memo Thủ công, dễ bỏ sót Tự động build-time
Conditional memo (early return) Không hỗ trợ tốt Hỗ trợ tốt
Over-memoization Rủi ro cao nếu không cẩn thận Heuristics skip cheap renders
Granularity Component-level Prop-level
Debug khi lỗi Tường minh trong code Cần DevTools plugin
Custom equality logic Hỗ trợ (tham số thứ 2) Không tùy chỉnh được
External libs (Zustand, Redux) Tốt Cần kiểm tra compatibility

Điểm compiler không thay thế được manual optimization: khi bạn cần custom equality function phức tạp, hoặc khi integrate với state management bên ngoài dùng mutable stores.

React DevTools hiện có plugin hỗ trợ visualize compiler output - bật lên để xem component nào đang được auto-memo, component nào bị skip.

Takeaway thực tế: nếu app của bạn có cascading re-renders (parent re-render kéo theo chuỗi child re-render không cần thiết), compiler giải quyết được hầu hết mà không cần bạn touch code. Nếu bottleneck đến từ heavy computation trong render (ví dụ filter 10,000 records), bạn vẫn cần useMemo thủ công vì đó là logic business, không phải vấn đề re-render.


Migrate từ React 18 lên React 19 + Compiler: từng bước

Migration không phải flip một switch. Mình đã thử trên một project Next.js 14 → 15 thực tế và có vài điểm cần lưu ý.

Bước 1: Upgrade dependencies

# Upgrade React 19 và Next.js 15
npm install react@19 react-dom@19 next@15

# Install compiler plugin
npm install -D babel-plugin-react-compiler

Bước 2: Enable compiler trong config

Nếu dùng Next.js 15, thêm vào next.config.js:

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

module.exports = nextConfig;

Hoặc nếu dùng Babel trực tiếp:

// babel.config.json
{
  "plugins": [["react-compiler", { "target": "react-19" }]]
}

343559 Migrate từng bước - đừng xóa useMemo cũ ngay, đo trước rồi quyết định sau

Bước 3: ĐỪNG xóa useMemo cũ ngay

Đây là sai lầm phổ biến nhất. Compiler và manual memo có thể cùng tồn tại. Xóa hết một lúc có thể tạo ra behavior thay đổi khó debug. Quy trình đúng:

  1. Enable compiler, giữ nguyên code cũ
  2. Chạy test suite đầy đủ
  3. Profile với React DevTools trước/sau
  4. Dần dần remove manual memo ở những chỗ đã verify compiler cover được

Bước 4: Fix các patterns block compiler

Chạy eslint với rule của compiler để detect tự động:

npm install -D eslint-plugin-react-compiler
// .eslintrc.json
{
  "plugins": ["react-compiler"],
  "rules": {
    "react-compiler/react-compiler": "error"
  }
}

Plugin này sẽ flag những pattern khiến compiler skip component của bạn - props mutation, non-deterministic values trong render, v.v.

Breaking changes cần biết: Rules of Hooks vẫn được enforce nghiêm hơn. Một số pattern "trốn" được ở React 18 sẽ throw error ở React 19. Thường gặp nhất là conditional hook calls và hooks trong loops.

Nếu bạn chưa familiar với React cơ bản, khóa Xây Dựng Website với ReactJS của F8 là chỗ tốt để nắm foundation trước khi nhảy vào các tính năng advanced này.


Best practices cho production app Việt Nam

Một vài điểm thực tế khi apply React 19 Compiler cho app production, đặc biệt các use case phổ biến trong thị trường Việt Nam: e-commerce, dashboard quản lý, app đặt hàng.

1. Kết hợp Server Components + Client Components hợp lý

Next.js 15 + React 19 cho phép bạn push phần lớn rendering lên server. Compiler tối ưu tốt nhất cho client components có state - server components không có state nên compiler không áp dụng nhiều ở đó.

// Server Component - fetch data, không state
async function ProductList() {
  const products = await getProducts(); // chạy trên server
  return <ProductGrid products={products} />; // client component
}

// Client Component - compiler auto-memo phần này
'use client';
function ProductGrid({ products }) {
  const [filter, setFilter] = useState('');
  // Compiler tự memo filtered list nếu products và filter không đổi
  const filtered = products.filter(p => p.name.includes(filter));
  return (...);
}

2. Với large lists - profile trước, optimize sau

App quản lý kho, báo cáo bán hàng, hay danh sách đơn hàng thường có lists lớn. Compiler giảm re-renders của parent, nhưng nếu bạn render 5,000 rows không có virtualization thì vẫn lag. Kết hợp compiler với react-window hoặc TanStack Virtual.

343560 Compiler giảm re-renders, virtualization giảm DOM nodes - 2 layer tối ưu khác nhau

3. Forms với useActionState trong app e-commerce

Checkout flow, form đặt hàng, form đăng nhập - tất cả benefit từ useActionState + Server Actions. Loading state tự quản lý, không cần thêm state riêng, compiler memo phần UI không liên quan đến form state.

4. Avoid những anti-pattern này

// ❌ Block compiler - object mới mỗi render
function BadComponent({ userId }) {
  const config = { id: userId, timestamp: Date.now() }; // non-deterministic!
  return <Child config={config} />;
}

// ✅ Compiler friendly
function GoodComponent({ userId }) {
  // Date.now() ra ngoài render, hoặc dùng useMemo nếu cần reactive
  return <Child userId={userId} />;
}

5. Monitoring sau khi deploy

Bật React DevTools Profiler trên staging, đo baseline trước khi push compiler lên production. Một số third-party components (charting libs, map libs) có thể có side effects trong render khiến compiler skip - cần wrap thêm.

Tóm lại:

  • Default: tin tưởng compiler, không cần thêm memo thủ công
  • Exception: custom equality logic, external mutable stores, heavy computation business logic
  • Action: bật eslint plugin, profile DevTools, fix patterns block compiler

Drop comment bên dưới nếu bạn gặp edge case nào khi migrate - mình sẽ cập nhật bài theo feedback thực tế.