React 19 Compiler chuyên sâu: Loại bỏ useMemo, useCallback & Điểm chuẩn Profiler

Mục lục bài viết(17 mục)
React 19 Compiler, với tên mã nội bộ là "Forget", thay đổi cơ bản mô hình đối chiếu của React bằng cách tự động hóa memoization. Hướng dẫn này trình bày chi tiết cơ chế hoạt động, cách tích hợp thực tế và ý nghĩa về hiệu suất của nó, tập trung vào việc loại bỏ các hook useMemo và useCallback thủ công.
Lộ trình Kiến trúc Next.js 15 & React 19
IR của Compiler và Memoization Tự động
React Compiler hoạt động như một Babel transform, phân tích mã nguồn JavaScript/TypeScript để xác định các giá trị ổn định về tham chiếu và tự động chèn các ranh giới memoization. Nguyên tắc cốt lõi của nó là chỉ render lại những phần cần thiết của UI bằng cách ngăn chặn việc thực thi lại không cần thiết các thân hàm component và tạo lại các đối tượng/hàm.
Intermediate Representation (IR) của compiler phân tích các hàm component để xác định biểu thức nào ổn định về tham chiếu qua các lần render. Nó xác định "các giá trị phản ứng" – props, state, context và các giá trị được dẫn xuất từ chúng – và theo dõi các dependency của chúng. Khi một component render lại, compiler sẽ tạo mã kiểm tra xem các dependency phản ứng này có thay đổi hay không. Nếu không, giá trị hoặc tham chiếu hàm đã được tính toán trước đó sẽ được sử dụng lại.
Hãy xem xét một component render một danh sách các mục. Nếu không có compiler, hàm renderItem và mảng memoizedData sẽ được tạo lại mỗi khi component cha render lại, ngay cả khi items và onClick không thay đổi.
// Before React 19 Compiler
import React, { useMemo, useCallback } from 'react';
interface Item {
id: string;
name: string;
}
interface MyListProps {
items: Item[];
onClick: (id: string) => void;
}
function MyList({ items, onClick }: MyListProps) {
// Manual memoization required for referential stability
const renderItem = useCallback((item: Item) => {
return (
<li key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</li>
);
}, [onClick]); // Dependency on onClick
const memoizedData = useMemo(() => {
return items.map(item => ({ ...item, processed: true }));
}, [items]); // Dependency on items
return (
<ul>
{memoizedData.map(renderItem)}
</ul>
);
}
export default MyList;
Với React 19 Compiler, các lời gọi useMemo và useCallback tường minh trở nên thừa thãi. IR của compiler phát hiện rằng renderItem và memoizedData được dẫn xuất từ props (items, onClick) và tự động bao bọc định nghĩa của chúng trong các kiểm tra memoization.
// After React 19 Compiler (conceptual, compiler inserts the actual memoization)
import React from 'react'; // No useMemo/useCallback needed
interface Item {
id: string;
name: string;
}
interface MyListProps {
items: Item[];
onClick: (id: string) => void;
}
function MyList({ items, onClick }: MyListProps) {
// Compiler automatically memoizes this function
const renderItem = (item: Item) => {
return (
<li key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</li>
);
};
// Compiler automatically memoizes this array creation
const memoizedData = items.map(item => ({ ...item, processed: true }));
return (
<ul>
{memoizedData.map(renderItem)}
</ul>
);
}
export default MyList;
Đầu ra của compiler cho MyList về mặt khái niệm sẽ giống như sau:
// Simplified conceptual output from React Compiler
function MyList(props) {
const { items, onClick } = props;
// Compiler-generated memoization for renderItem
const renderItem = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.memoize(
() => (item) => {
return React.createElement("li", {
key: item.id,
onClick: () => onClick(item.id)
}, item.name);
},
[onClick] // Compiler infers dependencies
);
// Compiler-generated memoization for memoizedData
const memoizedData = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.memoize(
() => items.map(item => ({ ...item, processed: true })),
[items] // Compiler infers dependencies
);
return React.createElement("ul", null, memoizedData.map(renderItem));
}
Hàm memoize nội bộ này là một biểu diễn đơn giản hóa; việc triển khai thực tế bao gồm các kiểm tra và tối ưu hóa phức tạp hơn. Điểm mấu chốt là compiler xử lý việc suy luận mảng dependency và logic memoization, giảm boilerplate và lỗi tiềm ẩn do con người.
Cấu hình và Tích hợp ESLint
Để bật React Compiler, bạn cần tích hợp nó vào pipeline build của mình. Đối với các thiết lập dựa trên Babel (như Create React App hoặc Next.js), điều này liên quan đến việc thêm plugin Babel.
Cấu hình Next.js
Đối với Next.js, bật experimental compiler trong next.config.js:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
reactCompiler: true, // Enable the React Compiler
},
};
module.exports = nextConfig;
Quy tắc ESLint
Gói eslint-plugin-react-compiler cung cấp các quy tắc linting để giúp bạn xác định các mẫu mã có thể ngăn compiler tối ưu hóa hiệu quả hoặc để loại bỏ các lời gọi useMemo/useCallback thừa thãi.
Cài đặt plugin:
npm install --save-dev eslint-plugin-react-compiler
# or
yarn add --dev eslint-plugin-react-compiler
Cấu hình .eslintrc.js của bạn (hoặc .eslintrc.json):
// .eslintrc.js
module.exports = {
// ... other ESLint configurations
plugins: [
// ... other plugins
'react-compiler',
],
rules: {
// ... other rules
'react-compiler/react-compiler': 'error', // Enable the compiler lint rule
},
};
Quy tắc này sẽ gắn cờ các trường hợp mà useMemo hoặc useCallback không còn cần thiết, hoặc nơi các mẫu mã có thể cản trở việc tối ưu hóa của compiler (ví dụ: các đối tượng có thể thay đổi trong dependencies).
Kiểm tra đầu ra được tạo và Điểm chuẩn Profiler
Để hiểu tác động của compiler, cần kiểm tra mã được tạo và lập hồ sơ hiệu suất thời gian chạy.
Kiểm tra mã được tạo
Sau khi bật compiler, đầu ra build của bạn sẽ chứa mã đã được chuyển đổi. Mặc dù việc kiểm tra trực tiếp đầu ra của Babel có thể dài dòng, nhưng nó xác nhận việc compiler đã được kích hoạt. Để có cái nhìn dễ hiểu hơn, hãy cân nhắc sử dụng các công cụ như AST Explorer với plugin Babel, hoặc kiểm tra các gói đã biên dịch trong thư mục dist của bạn.
Điều quan trọng là quan sát sự vắng mặt của các lời gọi useMemo và useCallback tường minh trong mã nguồn của bạn, và sự hiện diện của logic memoization do compiler tạo ra trong đầu ra đã biên dịch.
Chrome DevTools Profiler
Tab Performance của Chrome DevTools rất quan trọng để đo điểm chuẩn.
- Ghi lại một profile: Mở DevTools, điều hướng đến tab "Performance" và nhấp vào nút ghi. Tương tác với ứng dụng của bạn, kích hoạt việc render lại các component mà bạn đang tối ưu hóa.
- Phân tích Flame Chart: Tìm phần "User Timing". Bạn sẽ thấy các thời gian nội bộ của React, bao gồm cả việc render component.
- Xác định Re-renders: Tập trung vào luồng "Main". Các component không được tối ưu hóa sẽ hiển thị việc thực thi lại thường xuyên toàn bộ thân hàm của chúng. Với compiler, bạn sẽ quan sát thấy ít re-render hơn đối với các component mà props hoặc state của chúng không thay đổi.
- So sánh trước/sau: Chạy ứng dụng của bạn mà không bật compiler, ghi lại một profile, sau đó bật compiler và ghi lại một profile khác dưới các mẫu tương tác giống hệt nhau. So sánh thời gian "Render" và số lần thực thi hàm component.
Kịch bản Profiler ví dụ:
Xem xét một component cha App cập nhật một bộ đếm, khiến MyList render lại.
// App.tsx
import React, { useState } from 'react';
import MyList from './MyList'; // MyList from previous example
interface Item {
id: string;
name: string;
}
const initialItems: Item[] = Array.from({ length: 1000 }, (_, i) => ({
id: String(i),
name: `Item ${i}`,
}));
function App() {
const [count, setCount] = useState(0);
const handleClick = (id: string) => {
console.log(`Clicked item: ${id}`);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>Increment Count</button>
<MyList items={initialItems} onClick={handleClick} />
</div>
);
}
export default App;
Quan sát Profiler (Khái niệm):
| Metric | Không có Compiler (Memo thủ công) | Có Compiler (Memo tự động) |
|---|---|---|
MyList Render Count | 1 (ban đầu) + 1 (khi count thay đổi) | 1 (ban đầu) + 0 (khi count thay đổi) |
MyList Execution Time | ~50ms (ban đầu) + ~50ms (re-render) | ~50ms (ban đầu) + ~5ms (re-render, do kiểm tra memoization) |
renderItem Re-creation | Có, mỗi khi App re-render | Không, được compiler memoized |
memoizedData Re-creation | Có, mỗi khi App re-render | Không, được compiler memoized |
Lưu ý: Đây là các giá trị minh họa. Lợi ích hiệu suất thực tế phụ thuộc vào độ phức tạp của component và tần suất re-render.
Thông tin chính từ profiler sẽ là logic nội bộ của MyList (như việc tạo renderItem và memoizedData) bị bỏ qua khi props của nó (items, onClick) không thay đổi, ngay cả khi component cha App render lại.
Những điều cần lưu ý và Khắc phục sự cố trong Production
Mặc dù React Compiler đơn giản hóa memoization, nhưng nó cũng đưa ra những cân nhắc mới.
1. Thay đổi đối tượng và Tính bằng tham chiếu
Compiler phụ thuộc rất nhiều vào tính bằng tham chiếu. Nếu bạn thay đổi các đối tượng hoặc mảng được truyền dưới dạng props hoặc được sử dụng trong state, compiler sẽ không phát hiện ra sự thay đổi, dẫn đến dữ liệu cũ hoặc bỏ lỡ các lần re-render.
Vấn đề:
function BadComponent({ data }: { data: { value: number } }) {
// Compiler assumes 'data' is referentially stable if its reference doesn't change.
// Mutating 'data.value' directly will not trigger a re-render if 'data' itself is the same object.
data.value++; // DANGER: Direct mutation
return <div>Value: {data.value}</div>;
}
function Parent() {
const [obj, setObj] = useState({ value: 0 });
// This will NOT cause BadComponent to re-render when obj.value is mutated internally
// because the 'obj' reference itself doesn't change.
return <BadComponent data={obj} />;
}
Cách khắc phục: Luôn coi các đối tượng props và state là bất biến. Tạo các đối tượng mới khi cập nhật.
function GoodComponent({ data }: { data: { value: number } }) {
// Compiler correctly detects change if 'data' is a new object
return <div>Value: {data.value}</div>;
}
function Parent() {
const [obj, setObj] = useState({ value: 0 });
const updateValue = () => {
// Create a new object to ensure referential equality changes
setObj(prev => ({ ...prev, value: prev.value + 1 }));
};
return (
<>
<button onClick={updateValue}>Update Value</button>
<GoodComponent data={obj} />
</>
);
}
2. Closures nắm giữ các giá trị cũ
Mặc dù compiler xử lý memoization hàm, nhưng closures vẫn có thể nắm giữ các giá trị cũ nếu không được xử lý cẩn thận, đặc biệt với useEffect hoặc useLayoutEffect.
Vấn đề:
function StaleClosureComponent() {
const [count, setCount] = useState(0);
// Compiler memoizes 'logCount' but it captures 'count' from its initial render
// if 'count' is not explicitly listed as a dependency for the effect.
const logCount = () => {
console.log('Current count:', count);
};
useEffect(() => {
// This effect runs once and uses the 'logCount' from the initial render,
// which always logs 0, even if count updates.
const interval = setInterval(logCount, 1000);
return () => clearInterval(interval);
}, []); // Missing dependency: logCount
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
Cách khắc phục: Đảm bảo useEffect và useCallback (nếu vẫn được sử dụng cho các trường hợp đặc biệt hoặc tương thích thư viện bên ngoài) có các dependency chính xác. Compiler giúp với memoization cấp độ component, nhưng các dependency useEffect vẫn rất quan trọng để có hành vi chính xác.
function CorrectClosureComponent() {
const [count, setCount] = useState(0);
// Compiler memoizes 'logCount', and it will be re-created if 'count' changes.
// This is fine, as the effect will re-run and capture the new 'logCount'.
const logCount = () => {
console.log('Current count:', count);
};
useEffect(() => {
// Now, 'logCount' is a dependency. When 'count' changes, 'logCount' is re-created
// by the compiler, and this effect re-runs, capturing the new 'logCount' with the updated 'count'.
const interval = setInterval(logCount, 1000);
return () => clearInterval(interval);
}, [logCount]); // Correct dependency
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
Trong hầu hết các trường hợp, compiler sẽ làm cho logCount ổn định nếu count ổn định. Tuy nhiên, nếu count thay đổi, logCount sẽ được tạo lại. useEffect vẫn cần khai báo logCount là một dependency để đảm bảo effect đăng ký lại với hàm mới nhất. Đây là một điểm tinh tế mà compiler không loại bỏ nhu cầu quản lý dependency useEffect chính xác.
3. Thư viện bên thứ ba và Context Consumers
Một số thư viện bên thứ ba có thể không hoàn toàn tương thích với các giả định của compiler, đặc biệt là những thư viện dựa vào các đảm bảo ổn định tham chiếu cụ thể hoặc thực hiện so sánh sâu. Tương tự, các nhà cung cấp/người tiêu dùng context tùy chỉnh có thể cần được xem xét kỹ lưỡng.
Cách khắc phục: Kiểm tra kỹ lưỡng. Nếu bạn gặp phải hành vi không mong muốn, hãy tạm thời tắt compiler cho các tệp hoặc component cụ thể bằng cách sử dụng các chỉ thị /* @no-optimize */ hoặc /* @no-memo */ (kiểm tra tài liệu compiler để biết cú pháp chính xác) hoặc quay lại useMemo/useCallback thủ công cho những khu vực có vấn đề đó. Báo cáo vấn đề cho người duy trì thư viện.
Các câu hỏi thường gặp
1. React Compiler có loại bỏ tất cả nhu cầu về useMemo và useCallback không?
Không. Mặc dù nó giảm đáng kể việc sử dụng chúng cho các tối ưu hóa cấp độ component, nhưng vẫn có những trường hợp đặc biệt. Ví dụ, nếu bạn cần truyền một giá trị hoặc callback đã được memoized cho một thư viện bên thứ ba yêu cầu rõ ràng useMemo/useCallback cho các tối ưu hóa nội bộ của riêng nó, hoặc nếu bạn đang xử lý các phép tính rất phức tạp, không phản ứng mà compiler có thể không tối ưu hóa hoàn toàn. Tuy nhiên, đối với logic render component thông thường, chúng trở nên phần lớn thừa thãi.
2. Compiler xử lý các đối tượng hoặc hàm phức tạp được truyền dưới dạng props như thế nào?
Compiler theo dõi tính bằng tham chiếu. Nếu một đối tượng hoặc hàm phức tạp được truyền dưới dạng prop, và tham chiếu của nó vẫn giữ nguyên qua các lần render, compiler sẽ coi nó là ổn định. Nếu một đối tượng hoặc hàm mới được tạo trong mỗi lần render (ví dụ: onClick={() => doSomething()}), compiler sẽ phát hiện đây là một thay đổi và render lại các component phụ thuộc. Sức mạnh của compiler nằm ở việc tự động memoize các giá trị và hàm dẫn xuất trong một component, ngăn chặn việc tạo lại chúng nếu các dependency của chúng không thay đổi.
3. Chi phí hiệu suất của bản thân React Compiler là gì?
Compiler chạy tại thời điểm build, vì vậy không có chi phí thời gian chạy cho quá trình biên dịch. Mã được tạo bao gồm các kiểm tra bổ sung cho tính bằng tham chiếu, điều này tạo ra một chi phí thời gian chạy tối thiểu so với mã không được tối ưu hóa. Tuy nhiên, chi phí này hầu như luôn bị lu mờ bởi lợi ích hiệu suất từ việc ngăn chặn các lần re-render và tính toán lại không cần thiết, đặc biệt trong các ứng dụng phức tạp.
4. Tôi có thể từ chối sử dụng compiler cho các component hoặc tệp cụ thể không?
Có, React Compiler hỗ trợ các cơ chế từ chối. Bạn thường có thể thêm một chỉ thị comment đặc biệt ở đầu tệp hoặc hàm component (ví dụ: /* @no-optimize */ hoặc /* @no-memo */) để hướng dẫn compiler bỏ qua việc xử lý khối mã cụ thể đó. Điều này hữu ích cho việc gỡ lỗi hoặc cho các component có hành vi không mong muốn khi bật compiler. Tham khảo tài liệu chính thức của React Compiler để biết cú pháp chính xác.
5. React Compiler có hoạt động với các framework render phía máy chủ (SSR) như Next.js hoặc Remix không?
Có, React Compiler được thiết kế để hoạt động liền mạch với các framework SSR. Vì nó là một Babel transform, nó hoạt động trong quá trình build, ảnh hưởng đến cả các gói phía client và phía máy chủ. Điều này có nghĩa là lợi ích hiệu suất của memoization tự động áp dụng cho HTML được render ban đầu từ máy chủ cũng như quá trình hydration và cập nhật phía client sau đó.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

React Compiler trong Thực tế: Tự động Memoization, Quy tắc & Điểm chuẩn Hiệu suất
Hướng dẫn toàn diện về React Compiler trong thực tế: tự động memoization, quy tắc và điểm chuẩn hiệu suất với kiến trúc cấp độ production và ví dụ code.
Read more
React Thực Chiến: Hiểu Đúng Hooks, State Management và Next.js App Router
Hướng dẫn thực chiến về React Hooks từ cơ bản đến nâng cao — useState, useEffect, useRef, useContext, useMemo, useCallback, Custom Hooks — kết hợp với Zustand, TanStack Query và Next.js App Router. Giải thích tại sao, không chỉ là cách làm.
Read more
React 19 Actions trong thực tế: useActionState, useOptimistic & khả năng phục hồi của Server Action
Hướng dẫn toàn diện về React 19 Actions trong thực tế: useActionState, useOptimistic và khả năng phục hồi của Server Action với kiến trúc cấp độ sản phẩm và ví dụ mã.
Read more