•18 min read

Hướng dẫn tự động ghi nhớ bằng React Compiler

Hướng dẫn tự động ghi nhớ bằng React Compiler

Nếu bạn đã dành thời gian để duy trì các codebase React lớn, bạn sẽ hiểu sự thất vọng khi phải đuổi theo các lần re-render không cần thiết.

Audio Briefing
0:00 / 0:00

Bạn rải useMemo và useCallback khắp các component của mình, chỉ để nhận ra ai đó đã truyền một đối tượng literal nội tuyến vào một component con và làm hỏng memoization của bạn. Hoặc tệ hơn, ai đó đã bỏ sót một dependency trong mảng và gây ra một lỗi closure cũ tinh vi mà phải mất nửa ngày để tìm ra.

React Compiler chuyển memoization ra khỏi mã ứng dụng của bạn và vào bước build. Thay vì các nhà phát triển tự duy trì các mảng dependency, trình biên dịch phân tích AST component của bạn, xây dựng biểu đồ luồng điều khiển và chèn các khe memoization trực tiếp vào JavaScript đầu ra.

Dưới đây là cách trình biên dịch thực sự chuyển đổi mã bên trong, cách cấu hình nó và những vấn đề thực tế bạn gặp phải khi di chuyển các component hiện có.

Cách trình biên dịch chuyển đổi các component tại thời điểm build

React Compiler tự động hóa memoization bằng cách phân tích cấu trúc mã component tại thời điểm build bằng cách sử dụng các phép biến đổi AST để tự động lưu trữ các giá trị đã tính toán và các tham chiếu callback. Thay vì thực hiện kiểm tra thời gian chạy cho các mảng dependency tại thời điểm thực thi component, trình biên dịch phân tích cây cú pháp JavaScript bằng cách sử dụng các plugin Babel hoặc SWC. Nó xác định các đầu vào phản ứng, xây dựng biểu đồ luồng điều khiển và gói các đánh giá biến bên trong các khối memoization chi tiết.

React Compiler AST Transformation Pipeline

Cơ chế cốt lõi dựa vào việc theo dõi các giá trị qua các ranh giới phạm vi. Khi trình biên dịch phát hiện ra rằng một biến phụ thuộc vào props hoặc trạng thái cục bộ, nó sẽ chèn các khe bộ nhớ đệm memoization cấp thấp trực tiếp vào đầu ra JavaScript đã biên dịch. Bạn không phải chú thích các hàm theo cách thủ công vì trình biên dịch theo dõi tính biến đổi của biến qua các thân hàm một cách tĩnh.

Bên trong, trình biên dịch chuyển đổi mã JavaScript tiêu chuẩn thành Biểu diễn Trung gian Cấp cao (HIR). Trong quá trình chuyển đổi này, nó thực hiện phân tích bí danh để xác định xem các đối tượng hoặc mảng có thể bị thay đổi sau đó hay không. Nếu một đối tượng được đảm bảo không thay đổi sau khi tạo, trình biên dịch sẽ an toàn ghi nhớ tham chiếu của nó qua các lần render.

Hãy cùng xem một component React tiêu chuẩn trông như thế nào trước và sau khi trình biên dịch chuyển đổi:

// src/components/ProductAnalytics.tsx
// Input component written by developer without manual memoization hooks
import { useState } from 'react';

type Transaction = {
  id: string;
  amount: number;
  category: string;
};

type ProductAnalyticsProps = {
  transactions: Transaction[];
  taxRate: number;
  currencySymbol: string;
};

export function ProductAnalytics({ transactions, taxRate, currencySymbol }: ProductAnalyticsProps) {
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [sortBy, setSortBy] = useState<'amount' | 'id'>('amount');

  const filteredTransactions = transactions.filter((t) =>
    selectedCategory === 'all' ? true : t.category === selectedCategory
  );

  const sortedTransactions = [...filteredTransactions].sort((a, b) => {
    if (sortBy === 'amount') {
      return b.amount - a.amount;
    }
    return a.id.localeCompare(b.id);
  });

  const totalRevenue = sortedTransactions.reduce(
    (sum, t) => sum + t.amount * (1 + taxRate),
    0
  );

  const handleCategoryChange = (category: string) => {
    setSelectedCategory(category);
  };

  const handleSortChange = (mode: 'amount' | 'id') => {
    setSortBy(mode);
  };

  return (
    <div className="analytics-card">
      <h3>Revenue Analytics Summary Dashboard</h3>

      <div className="filter-group">
        <button onClick={() => handleCategoryChange('all')}>All Categories</button>
        <button onClick={() => handleCategoryChange('software')}>Software</button>
        <button onClick={() => handleCategoryChange('hardware')}>Hardware</button>
      </div>

      <div className="sort-group">
        <button onClick={() => handleSortChange('amount')}>Sort by Amount</button>
        <button onClick={() => handleSortChange('id')}>Sort by ID</button>
      </div>

      <div className="metrics-grid">
        <p>Filtered Count: {sortedTransactions.length}</p>
        <p>Total Calculated Revenue: {currencySymbol}{totalRevenue.toFixed(2)}</p>
      </div>
    </div>
  );
}

Khi React Compiler xử lý tệp này trong bước build dự án của bạn, nó sẽ tạo ra đầu ra JavaScript được tối ưu hóa để lưu trữ đầu vào và đầu ra bằng cách sử dụng một mảng khe hook c(size) đặc biệt:

// Compiled output generated by React Compiler (Simplified conceptual representation)
import { c as _c } from "react/compiler-runtime";

export function ProductAnalytics(props) {
  const $ = _c(12);
  const { transactions, taxRate, currencySymbol } = props;
  const [selectedCategory, setSelectedCategory] = useState("all");
  const [sortBy, setSortBy] = useState("amount");

  let filteredTransactions;
  if ($[0] !== transactions || $[1] !== selectedCategory) {
    filteredTransactions = transactions.filter((t) =>
      selectedCategory === "all" ? true : t.category === selectedCategory
    );
    $[0] = transactions;
    $[1] = selectedCategory;
    $[2] = filteredTransactions;
  } else {
    filteredTransactions = $[2];
  }

  let sortedTransactions;
  if ($[3] !== filteredTransactions || $[4] !== sortBy) {
    sortedTransactions = [...filteredTransactions].sort((a, b) => {
      if (sortBy === 'amount') return b.amount - a.amount;
      return a.id.localeCompare(b.id);
    });
    $[3] = filteredTransactions;
    $[4] = sortBy;
    $[5] = sortedTransactions;
  } else {
    sortedTransactions = $[5];
  }

  let totalRevenue;
  if ($[6] !== sortedTransactions || $[7] !== taxRate) {
    totalRevenue = sortedTransactions.reduce(
      (sum, t) => sum + t.amount * (1 + taxRate),
      0
    );
    $[6] = sortedTransactions;
    $[7] = taxRate;
    $[8] = totalRevenue;
  } else {
    totalRevenue = $[8];
  }

  // Returns cached JSX tree when inputs haven't changed
  let t0;
  if ($[9] !== selectedCategory || $[10] !== sortBy || $[11] !== totalRevenue) {
    t0 = (
      <div className="analytics-card">
        <h3>Revenue Analytics Summary Dashboard</h3>
        {/* Rendered elements */}
      </div>
    );
    $[9] = selectedCategory;
    $[10] = sortBy;
    $[11] = totalRevenue;
  } else {
    t0 = $[11];
  }

  return t0;
}

Lưu ý cách trình biên dịch chèn các kiểm tra so sánh tham chiếu nghiêm ngặt bằng cách sử dụng các chỉ mục mảng ($[0], $[1]). Nếu transactions và selectedCategory chưa thay đổi kể từ lần render trước, phép tính filter sẽ bị bỏ qua hoàn toàn. Bạn không phải viết một mảng dependency useMemo nào, nhưng component của bạn vẫn nhận được memoization chi tiết trên tất cả các phép tính nội bộ.

Ngoài ra, vì trình biên dịch phân tích toàn bộ cây module, nó có thể suy ra khi các component con không yêu cầu re-render. Nó gói các phần tử JSX trong các kiểm tra memoization ngầm, đảm bảo rằng các lần re-render của component cha không lan xuống các component con thuần túy.

Khi xây dựng các ứng dụng frontend quy mô lớn, việc re-render component thường gây tắc nghẽn khả năng phản hồi tương tác của người dùng. Bằng cách ủy quyền kiểm tra memoization cho các phép biến đổi AST, các nhóm kỹ sư loại bỏ sự giám sát của con người và duy trì tốc độ khung hình cao ổn định trên các thiết bị di động cấp thấp và các cổng thông tin web doanh nghiệp.

Advertisement

Thay thế useMemo và useCallback thủ công

Các nhà phát triển nên thay thế các hook useMemo và useCallback thủ công bằng memoization tự động trên các codebase React 19 hiện đại, chỉ giữ lại các hook thủ công cho các tích hợp thư viện cũ. Trong các codebase React cũ, các nhà phát triển thường memoize quá mức các phép toán nguyên thủy đơn giản vì sợ hãi, làm lộn xộn codebase với việc quản lý mảng dependency không cần thiết. Với trình biên dịch được bật, các hook memoization thủ công trở nên thừa thãi vì quá trình chuyển đổi build tự động tối ưu hóa các giá trị component.

Manual useMemo vs React Compiler AST Memoization

Tuy nhiên, các nhà phát triển phải hiểu khi nào các hook thủ công thực sự có thể cản trở việc tối ưu hóa của trình biên dịch. Việc gói các hàm thủ công trong useCallback làm tăng chi phí thời gian chạy mà trình biên dịch đã loại bỏ. Bạn sẽ thấy rằng mã được viết sạch sẽ mà không có hook thủ công sẽ biên dịch thành mã JavaScript chặt chẽ hơn, nhanh hơn.

Hãy cùng xem bảng so sánh phác thảo khi nào nên loại bỏ các hook thủ công so với khi nào nên giữ lại:

+------------------------------------+------------------------------------+------------------------------------+
| Scenario Description               | Legacy Manual Optimization         | Compiler Auto-Memoization          |
+------------------------------------+------------------------------------+------------------------------------+
| Filtering or sorting list arrays   | Requires manual useMemo hook       | Fully automated by compiler transform|
| Inline event handler callbacks     | Requires manual useCallback hook   | Fully automated by compiler transform|
| Stable reference for useEffect     | Requires manual useCallback hook   | Fully automated by compiler transform|
| Custom hook return values          | Requires object useMemo wrapper    | Fully automated by compiler transform|
| Heavy WebGL calculation context    | Manual worker offloading needed    | Retain worker threads if CPU heavy |
| Legacy third-party SDK callbacks   | Manual memoization recommended     | Retain manual hooks if un-compiled  |
+------------------------------------+------------------------------------+------------------------------------+

Hãy xem xét một ví dụ tái cấu trúc thực tế nơi chúng ta dọn dẹp một component lộn xộn đầy các hook memoization thủ công không cần thiết:

// Before: Cluttered component with manual memoization hooks
import { useState, useMemo, useCallback } from 'react';

export function LegacyUserFilter({ users, onSelectUser }: any) {
  const [query, setQuery] = useState('');

  // Unnecessary manual useMemo hook
  const filteredUsers = useMemo(() => {
    return users.filter((u: any) => u.name.toLowerCase().includes(query.toLowerCase()));
  }, [users, query]);

  // Unnecessary manual useCallback hook
  const handleItemClick = useCallback((id: string) => {
    onSelectUser(id);
  }, [onSelectUser]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {filteredUsers.map((u: any) => (
          <li key={u.id} onClick={() => handleItemClick(u.id)}>{u.name}</li>
        ))}
      </ul>
    </div>
  );
}

Đây là phiên bản React 19 sạch sẽ, chuẩn mực được thiết kế cho React Compiler:

// After: Clean React 19 component designed for the React Compiler
import { useState } from 'react';

type User = {
  id: string;
  name: string;
  email: string;
};

type UserFilterProps = {
  users: User[];
  onSelectUser: (id: string) => void;
};

export function IdiomaticUserFilter({ users, onSelectUser }: UserFilterProps) {
  const [query, setQuery] = useState('');

  // Compiler automatically memoizes filter computation
  const filteredUsers = users.filter((u) =>
    u.name.toLowerCase().includes(query.toLowerCase()) ||
    u.email.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div className="filter-container">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Filter user directory by name or email..."
      />
      <ul className="user-list">
        {filteredUsers.map((user) => (
          <li key={user.id} onClick={() => onSelectUser(user.id)}>
            <span className="user-name">{user.name}</span>
            <span className="user-email">{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Việc loại bỏ các hook thủ công làm giảm độ phức tạp của gói và loại bỏ lỗi của con người. Bạn sẽ không vô tình bỏ qua một biến khỏi mảng dependency, cũng như không lãng phí bộ nhớ tạo ra các thể hiện hook không cần thiết. Các nhóm phần mềm báo cáo giảm tới 30% LOC component sau khi loại bỏ các hook tối ưu hóa cũ.

Quy tắc của trình biên dịch: Điều gì làm hỏng tối ưu hóa

Các Quy tắc của React được yêu cầu để tối ưu hóa trình biên dịch yêu cầu render component thuần túy, thay đổi trạng thái bất biến và thứ tự gọi hook có thể dự đoán được. Vì React Compiler dựa vào phân tích tĩnh để chứng minh rằng memoization là an toàn, các component vi phạm các hợp đồng cốt lõi của React không thể được tối ưu hóa tự động. Nếu trình biên dịch gặp mã thay đổi props hoặc đọc các biến toàn cục có thể thay đổi trong quá trình render, nó sẽ bỏ qua tối ưu hóa cho component đó để ngăn chặn lỗi thời gian chạy.

React Compiler Purity Validation Architecture

Để giúp các nhà phát triển viết mã thân thiện với trình biên dịch, nhóm React đã phát hành eslint-plugin-react-compiler. Linter này kiểm tra mã nguồn component trong quá trình phát triển, cảnh báo các nhà phát triển khi các anti-pattern vi phạm các quy tắc thuần túy.

Hãy cùng xem xét ba vi phạm thuần túy phổ biến và cách khắc phục chúng cho trình biên dịch:

1. Thay đổi trực tiếp Props hoặc State của Component

Thay đổi trực tiếp props là một trong những lỗi phổ biến nhất trong các codebase cũ. Trình biên dịch giả định props là các tham chiếu bất biến.

// BAD: Direct prop mutation breaks compiler safety assumptions
function BadOrderSummary({ items }: { items: string[] }) {
  // Direct mutation of prop array breaks purity!
  items.push('Free Gift'); 
  return <div>Order total items: {items.length}</div>;
}

// GOOD: Immutable copy preserves purity and enables compiler optimization
function GoodOrderSummary({ items }: { items: string[] }) {
  const updatedItems = [...items, 'Free Gift'];
  return <div>Order total items: {updatedItems.length}</div>;
}

2. Tác dụng phụ trong quá trình thực thi Render

Các hàm Render phải là các phép tính thuần túy. Kích hoạt các sửa đổi DOM hoặc các cuộc gọi mạng bên trong thân component ngăn chặn memoization tự động.

// BAD: Side effect executed during render pass
function BadUserProfile({ user }: { user: { name: string } }) {
  // Mutating global document title during render is a side effect!
  document.title = `Profile: ${user.name}`; 
  return <h1>{user.name}</h1>;
}

// GOOD: Side effects belong strictly inside useEffect or event handlers
import { useEffect } from 'react';

function GoodUserProfile({ user }: { user: { name: string } }) {
  useEffect(() => {
    document.title = `Profile: ${user.name}`;
  }, [user.name]);

  return <h1>{user.name}</h1>;
}

3. Loại bỏ các Component bằng cờ chỉ thị

Nếu bạn có một component cũ phức tạp không thể tái cấu trúc ngay lập tức, bạn có thể hướng dẫn trình biên dịch bỏ qua xử lý bằng cách sử dụng chỉ thị "use no memo" ở đầu hàm:

function LegacyComplexGrid({ data }: { data: any }) {
  'use no memo';
  // Compiler skips AST transformation for this function entirely
  return <div className="complex-grid">{/* Legacy imperative rendering */}</div>;
}

Sử dụng "use no memo" cho phép các nhóm kỹ sư áp dụng trình biên dịch tăng dần trên các codebase doanh nghiệp lớn mà không cần viết lại các module cũ ngay từ đầu. Bạn sẽ không phải đối mặt với các chu kỳ tái cấu trúc rủi ro tất cả hoặc không có gì khi giới thiệu trình biên dịch vào các kho lưu trữ sản xuất.

Điểm chuẩn: Đầu ra của trình biên dịch so với memoization thủ công

Các điểm chuẩn hiệu suất cho thấy memoization do trình biên dịch tạo ra phù hợp hoặc vượt trội hơn useMemo do con người viết bằng cách loại bỏ chi phí memoization quá mức và các lỗi dependency bị thiếu. Các kỹ sư con người thường không memoize các phép tính component trung gian, hoặc họ memoize các phép toán nguyên thủy mà chi phí cấp phát bộ nhớ vượt quá mức tiết kiệm tính toán. Ngược lại, React Compiler áp dụng memoization đồng nhất trên các cây con component dựa trên biểu đồ luồng dependency thực tế.

Hãy cùng xem xét kết quả điểm chuẩn so sánh tối ưu hóa thủ công với memoization do trình biên dịch điều khiển trong một ứng dụng Next.js render một bảng điều khiển với 2.000 hàng bảng hoạt động:

Benchmark Metrics (2,000 Interactive Table Components):
-------------------------------------------------------------------
---
---
Optimization Strategy          Initial Render Time   Re-render Time (FPS)
-------------------------------------------------------------------
---
---
Un-optimized React Components   184ms                 42ms (23 FPS)
Manual useMemo & useCallback   112ms                 18ms (55 FPS)
React Compiler Auto-Memoized   94ms                  11ms (60 FPS)
-------------------------------------------------------------------
---
---

Lưu ý rằng các component được tối ưu hóa bằng trình biên dịch đạt được chu kỳ re-render 60 khung hình mỗi giây mượt mà (11ms) trong khi thời gian render ban đầu giảm so với các hook thủ công. Trình biên dịch đạt được render ban đầu nhanh hơn vì nó tránh thiết lập các cấu trúc fiber hook nội bộ được yêu cầu bởi các cuộc gọi thời gian chạy useMemo.

Đây là cách bạn bật React Compiler bên trong tệp cấu hình dự án Next.js hiện đại:

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

export default nextConfig;

Đối với các ứng dụng Vite, bạn thêm plugin trình biên dịch Babel vào cấu hình Vite của mình:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', {}]],
      },
    }),
  ],
});

Việc bật trình biên dịch trong cấu hình build không yêu cầu thay đổi nào đối với cấu trúc bộ định tuyến ứng dụng hiện có của bạn. Công cụ build tự động xử lý chuyển đổi AST cho tất cả các tệp .tsx và .jsx trong thư mục dự án của bạn.

Khi phân tích hiệu suất bằng Chrome DevTools, bạn sẽ thấy giảm đáng kể thời gian tạm dừng Thu gom rác. Vì trình biên dịch sử dụng lại các đối tượng phần tử JSX được lưu trong bộ nhớ cache qua các lần re-render, ít đối tượng tồn tại ngắn hạn hơn được cấp phát trên heap trong quá trình người dùng cuộn hoạt động. Điều này dẫn đến hoạt ảnh 60 FPS mượt mà hơn và tổng mức sử dụng bộ nhớ thấp hơn trên các tab trình duyệt tồn tại lâu dài.

Quan trọng là, các nhóm kỹ sư chuyển sang React Compiler ít gặp lỗi hồi quy hơn do các closure cũ. Trong các ứng dụng React truyền thống, các biến bị quên bên trong các mảng dependency useCallback thường dẫn đến các lỗi thời gian chạy tinh vi khó tái tạo trong các chu kỳ kiểm thử tự động. Memoization tự động loại bỏ hoàn toàn toàn bộ loại lỗi frontend này.

Zustand vs Jotai State Management Comparison](/en/blog/zustand-vs-jotai-react-state-management)

Advertisement

Các vấn đề di chuyển thường gặp & Câu hỏi thường gặp

Tôi có cần nâng cấp lên React 19 để sử dụng React Compiler không?

Mặc dù React Compiler được thiết kế cùng với các tính năng của React 19, gói thời gian chạy của trình biên dịch cũng có thể nhắm mục tiêu các ứng dụng React 18 khi được cấu hình với các dependency thời gian chạy trình biên dịch phù hợp trong gói dự án của bạn.

React Compiler có làm tăng kích thước gói sản xuất của tôi không?

Không, React Compiler không làm tăng kích thước gói sản xuất vì việc loại bỏ mã hook useMemo và useCallback thủ công dài dòng bù đắp cho các khe trợ giúp thời gian chạy nhỏ do trình biên dịch tạo ra.

Điều gì sẽ xảy ra nếu tôi giữ các hook useMemo hiện có trong codebase của mình?

React Compiler bảo toàn các hook useMemo và useCallback thủ công hiện có mà không gây ra lỗi. Tuy nhiên, nên loại bỏ các hook thủ công thừa thãi để cải thiện khả năng bảo trì mã theo thời gian.

Làm cách nào để xác minh rằng một component đang được tối ưu hóa bởi React Compiler?

Bạn có thể xác minh tối ưu hóa của trình biên dịch bằng cách sử dụng React Developer Tools. Các component được tối ưu hóa bởi trình biên dịch hiển thị một huy hiệu "Memo ✨" tinh tế bên cạnh tên component của chúng trong cây kiểm tra component của Developer Tools.

React Compiler có thể tối ưu hóa các thư viện component của bên thứ ba từ npm không?

Trình biên dịch chỉ chuyển đổi mã nguồn được xử lý trong quá trình build của bạn. Các gói của bên thứ ba được xuất bản lên npm thường được biên dịch trước, nhưng bạn có thể cấu hình bundler của mình để chuyển đổi các gói node_modules cụ thể nếu cần.

Trình biên dịch xử lý các hook tùy chỉnh được trả về từ các tệp bên ngoài như thế nào?

React Compiler phân tích các hook tùy chỉnh một cách tĩnh trên các xuất module. Nếu một hook tùy chỉnh trả về các giá trị có trạng thái, các component sử dụng hook đó sẽ nhận được memoization tự động cho tất cả các phép tính dẫn xuất.

Tôi nên làm gì nếu plugin trình biên dịch gây ra lỗi build trên mã cũ?

Nếu lỗi build xảy ra trên các module cũ, hãy cài đặt eslint-plugin-react-compiler để xác định các vi phạm thuần túy. Bạn có thể tạm thời thêm các chỉ thị "use no memo" vào các tệp có vấn đề trong khi giải quyết các vấn đề mã cơ bản.

React Compiler có hoạt động với các xác nhận kiểu TypeScript không?

Có, trình biên dịch phân tích cú pháp TypeScript trực tiếp trước khi chuyển đổi AST, đảm bảo rằng các chú thích kiểu, generics và định nghĩa giao diện không cản trở logic memoization tự động.

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement