React 编译器(React Compiler)深度指南

React 编译器(React Compiler)深度指南

本文将系统介绍 React 编译器是什么、解决什么问题、如何启用与优雅使用、开发过程中的注意事项、常见陷阱与修复、性能评估方法以及团队落地实践。

1. 什么是 React 编译器?

  • React 编译器是一个针对 React 函数组件与 Hooks 的静态优化器(编译期优化工具)。
  • 它通过静态分析你的组件渲染代码,自动插入“安全的记忆化”和“稳定引用”优化,从而减少不必要的重新渲染与重复计算。
  • 目标是让你“按直觉编写 React”:少写或不写 useMemo/useCallback/memo 等手工优化,也能获得接近甚至优于手工优化的性能,且不改变可观察语义。

一句话总结:把你脑中“什么时候该 useMemo/useCallback/memo ”的经验交给编译器做,让业务代码更直白、更易维护。

2. 它解决了哪些痛点?

  • 过度或不足的手工优化
    • 开发者常在“提前优化”和“性能债务”之间摇摆,易出错且影响可读性。
  • 稳定引用问题
    • 子组件依赖的 props 频繁变更导致不必要渲染;函数、对象、数组在每次渲染都创建新引用。
  • 派生数据重复计算
    • map/filter/reduce/sort 等昂贵计算在每次渲染都重复执行。
  • 自定义 Hook、上下文(Context)导致的级联重渲染
    • 上下文值或 Hook 返回对象在没有必要时仍更新,传染式触发渲染。
  • 性能优化的“知识门槛”
    • 新人很难拿捏何时应该 memo/useMemo/useCallback;编译器试图把这些“知识”固化到工具里。

3. 编译器的大致工作原理(高层)

  • 静态分析渲染函数
    • 识别“响应式来源”(props、state、context、hook 的返回值)与它们形成的派生链条。
  • 构建依赖图
    • 跟踪表达式、对象、函数对响应式来源的依赖,形成一个细粒度的“数据流图”。
  • 自动记忆化与稳定引用
    • 对纯表达式、对象字面量、数组、函数等在依赖未变化时“缓存结果”,从而保持引用稳定、避免重复计算。
  • 有效剖分渲染子图
    • 将组件内部计算拆分为多个可独立重用/重算的子块,只有受影响的子块才会在依赖更新时重算。
  • 语义保持
    • 不改变外部可观察行为,优化结果与未优化的程序在逻辑上等价(但执行次数减少)。

你可以将它理解为:为你的组件“自动生成高质量 useMemo/useCallback”的机器。

4. 能与不能

  • 重点支持
    • 函数组件与 Hooks
    • React 18+。在现代框架(如 Next.js 15+)和工具链中集成更好
    • 浏览器端与 React Native(通过 Babel/Metro)皆可
  • 不改变
    • React 的编程模型不变(不是 Signals 或框架替代品)
    • 运行时 API 不变(没有新的 Hook 必须使用)
  • 约束与限制
    • 仅对“纯渲染”安全有效。渲染期间的副作用(如 Date.now() / Math.random() /访问可变的外部状态)会削弱或阻止优化,编译器和 ESLint 插件会提示
    • 类组件不是优化重点;核心收益来自函数组件与 Hook
    • 动态代码模式(如动态属性访问 obj[key] 的某些场景)可能降低可分析性,触发“放弃优化”或警告
    • 对 Server Components 无需编译器介入(它们不在客户端重渲染的路径上);编译器主要针对 Client Components

5. 如何启用

具体启用方式随工具链略有差异。以下是常见方案。

5.1 Next.js 15+

  • next.config.js 开启实验开关:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true
  }
}
module.exports = nextConfig
  • 建议同时开启 ESLint 规则(见“开发体验与 ESLint”)。

说明:

  • Next.js 使用其内置编译器(SWC)集成 React 编译器,无需单独配置 Babel。
  • 仅对 Client Components 生效;Server Components 会被跳过。

5.2 Vite(或其他使用 Babel 的构建链)

  • 安装 Babel 插件:
npm i -D babel-plugin-react-compiler
# 或
yarn add -D babel-plugin-react-compiler
  • 在 Babel 配置中启用:
// .babelrc 或 babel 配置节
{
  "presets": ["@babel/preset-react", "@babel/preset-typescript"],
  "plugins": ["babel-plugin-react-compiler"]
}
  • 对于 Vite,若使用 @vitejs/plugin-react,可通过其 babel 选项注入该插件,或直接使用 .babelrc

5.3 React Native(Metro)

  • React Native 默认走 Babel 管线,只需在 babel.config.js 中加入插件:
// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: ['babel-plugin-react-compiler'],
};
  • 建议与 ESLint 插件配合使用,尽早发现不纯渲染。

5.4 Remix、CRA 等

  • 原理同 Vite/Babel:确保最终打包管线使用 Babel,并加入插件。
  • CRA 已趋于过时,如仍使用,参考其自定义 Babel 配置方式或迁移到 Vite/Next。

6. 开发体验与 ESLint 配套

强烈建议启用 ESLint 插件,帮助你写出“可被编译器有效优化”的代码。

  • 安装:

    • eslint
    • eslint-plugin-react
    • eslint-plugin-react-hooks
    • eslint-plugin-react-compiler(用于编译器可优化性规则)
  • 典型配置(示例):

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:react-compiler/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2023,
    "sourceType": "module"
  },
  "settings": {
    "react": { "version": "detect" }
  }
}

说明:

  • 插件会对“渲染不纯”“依赖不可追踪”“可变外部访问”等发出警告或错误,帮助你修复以解锁编译器优化。
  • 仍需保留 react-hooks 规则,保证 Hooks 使用的正确性。

7. 如何优雅地使用(编写风格与最佳实践)

目标:写“直白的 React”,让编译器做优化,而不是你手动铺满 useMemo/useCallback

7.1 基本指导原则

  • 让渲染保持“纯”
    • 渲染期间不要做副作用(网络、日志、计时、随机数、读取可变单例、修改外部变量)
    • 需要副作用时放在 useEffect/useLayoutEffect
  • 使用不可变数据思维
    • 不要在渲染中就地修改对象/数组;用展开、map/filter/reduce 构造新值
  • 直接写派生数据
    • 先写出 items.map(...)、复杂排序/分组等的“朴素版本”,交给编译器做记忆化;不要过早手写 useMemo
  • 直接写内联对象与函数
    • 内联 {a, b}() => {...} 是可以的,编译器会在依赖不变时稳定其引用
  • 使用 const、纯函数和明确依赖
    • 帮助编译器构建依赖图,提高优化机会

示例(无需手写 useMemo/useCallback):

function Parent({ items, theme }) {
  // 昂贵的派生计算
  const expensive = items
    .filter(x => x.active)
    .map(x => ({ id: x.id, label: x.name.toUpperCase() }))
    .sort((a, b) => a.label.localeCompare(b.label));

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

  return (
    <List
      data={expensive}
      options={{ theme, pageSize: 20 }}  // 内联对象
      onSelect={onSelect}                // 内联函数
    />
  );
}

有了编译器,上述 data/options/onSelect 的引用会在其依赖未变时保持稳定,避免无谓的子组件重渲染。

7.2 何时仍然需要手工 useMemo/useCallback/useRef

  • 与非 React 世界交互,且“引用稳定性”本身是协议的一部分
    • DOM 原生 addEventListener/removeEventListener:需要稳定的回调引用,或在 Effect 中正确地注册/反注册
    • 某些第三方库用引用相等性做缓存/订阅键
  • 生命周期内“只创建一次”的值
    • 常量句柄、第三方实例等可用 useRef 或 useState(() => init) 初始化一次
  • 极端的性能热点且编译器难以分析
    • 高度动态的访问模式导致编译器放弃优化时,可针对性使用 useMemo 并清晰声明依赖
  • 跨渲染稳定回调(React 19)
    • 如果你使用 React 19 的 useEffectEvent(或相关 API),可更优雅地处理 Effect 中的稳定回调问题

经验法则:

  • 优先让编译器做事;遇到明确的边界协议或编译器无法优化的路径,再用 useMemo/useCallback/useRef 做最小化补充。

7.3 自定义 Hook 编写建议

  • Hook 的“渲染部分”也必须纯
    • 不在 Hook 的顶层渲染路径做副作用;副作用放进 Hook 内部的 useEffect
  • 尽量返回稳定的结构
    • 返回对象/数组在依赖未变时应表现为稳定的形状与引用
  • 避免隐藏的可变共享状态
    • 模块级可变单例(如 let cache = {})在渲染中访问会破坏优化与可预测性

7.4 Context 使用建议

  • Provider 的 value 可以是内联对象,由编译器稳定引用
  • 将不相关的 context 切分,降低过度渲染范围(编译器在一些场景能减少影响,但不要依赖它“魔法”解决所有 context 级联渲染)
  • 对于超大对象的 context,优先考虑拆分或选择器方案(若框架/库支持)

7.5 列表与 keys

  • keys 依然重要
    • 编译器不会修正错误的 key 使用
  • 列表项子组件可以更大胆地使用内联 props
    • 编译器会稳定其引用,减少子项重渲染

8. 常见陷阱与修复建议

以下模式会阻碍编译器优化,ESLint 插件通常会给出提示。

  • 在渲染中访问不可追踪的可变值
    • 例如:读取模块级可变单例、window.someMutable、可变的 Map/Set
    • 修复:把这些值纳入 React 的数据流(通过 props/state/context),或在 Effect 中读写
  • 在渲染中调用不纯函数
    • 例如:Math.random()Date.now()、带副作用的日志/埋点
    • 修复:移入 Effect;需要“首渲染时生成一次”的值可用 useState(() => Date.now())
  • 动态属性访问导致依赖不明确
    • 例如:obj[key] 的 key 来源于外部不可追踪输入
    • 修复:显式列出依赖,或重构为可分析的分支
  • 直接修改 props 或 state 的子结构
    • 例如:props.list.push(x)
    • 修复:使用不可变操作,返回新数组/对象
  • 读取 ref.current 决定渲染逻辑
    • ref 的变更不是响应式来源,编译器不追踪
    • 修复:若渲染依赖该值,应将其提升为 state 或通过 props/context 传递

9. 性能评估与观测

  • 前后对比测试
    • 使用 React DevTools Profiler 对关键交互录制,比较提交次数、渲染耗时、组件重渲染数量
  • 指标选择
    • CPU-bound 页面:交互卡顿、输入延迟、滚动掉帧
    • RN/移动端:FPS、JS 线程占用、长任务比例
  • 观察缓存命中率(间接)
    • 重渲染次数减少通常伴随内联对象/函数引用稳定
  • 线上灰度
    • 将编译器在小流量或某些页面先行开启,观测错误率与性能指标

提示:

  • 编译器本身可能增加少量构建时开销;运行时通常能显著降低多余工作量。
  • 对极度微小的组件,过度记忆化可能带来很轻微的常数开销,但总体收益常在应用级联重渲染中体现。

10. 迁移与团队落地实践

  • 渐进启用
    • 优先在最痛的页面/包开启,再扩大范围
  • 强制 ESLint 规则
    • 在 CI 中启用 react-compiler 相关规则,保证代码可优化性
  • 编码规范更新
    • 鼓励“直白编码”,少写手工 useMemo/useCallback;将“只创建一次”的资源统一用 useRef/useState 初始化
  • 监控与回退
    • 通过配置开关支持快速禁用(按项目或按文件);遇到疑难问题可快速定位对比
  • 代码评审关注点
    • 关注“纯渲染”、数据不可变、避免隐藏的可变外部依赖

关于按文件/按函数禁用:

  • 各工具链通常支持通过注释或配置对特定文件/片段禁用编译器优化。具体注释指令名称与范围以所用插件/框架文档为准(建议在确有需要时使用,并附上原因)。

11. FAQ

  • 它是 Signals 吗?会替代 React 吗?
    • 不是。它是 React 之上的编译期优化,不改变 React 的模型与 API。
  • 是否可以删除所有 useMemo/useCallback/memo
    • 不是“所有”。大多数“为子组件稳定引用”与“避免重复计算”的场景可以删除;与第三方协议、DOM 事件、一次性实例等相关的稳定性需求仍建议保留或改用合适手段(useRef/useEffect/useEffectEvent 等)。
  • 类组件能受益吗?
    • 主要优化对象是函数组件与 Hooks。类组件不是重点。
  • SSR/Server Components 受影响吗?
    • 编译器主要作用于客户端渲染路径;Server Components 本身不在客户端重渲染,不需要编译器优化。
  • 会不会破坏语义或引入难以定位的 Bug?
    • 目标是“语义保持”。若编译器无法证明优化安全,会保守地放弃优化并给出提示。建议开启 ESLint 规则与渐进灰度。
  • 与第三方库是否有兼容性问题?
    • 一般无感。若第三方库强依赖对象/函数的“跨渲染稳定引用”,建议保留手工稳定化(useMemo/useCallback/useRef),或在 Effect 中正确管理订阅。
  • 会影响构建时间吗?
    • 视项目规模与代码结构而定,构建时间可能略有增加;通常换来运行时显著节省的渲染/计算成本。

12. 示例:前后对比

12.1 传统手工优化写法

function Toolbar({ theme, onSaveRaw, items }) {
  const data = useMemo(() => {
    return items.filter(x => x.active).map(x => ({ id: x.id, n: x.name.length }));
  }, [items]);

  const onSave = useCallback(() => {
    onSaveRaw({ ts: Date.now() }); // 非纯,且依赖 now
  }, [onSaveRaw]);

  const options = useMemo(() => ({ theme, size: 'm' }), [theme]);

  return <Panel data={data} options={options} onSave={onSave} />;
}

问题:

  • 为稳定引用消耗心智
  • Date.now() 在渲染期间会破坏纯性(应该挪到 Effect 或交互时机)

12.2 启用编译器后的直白写法

function Toolbar({ theme, onSaveRaw, items }) {
  const data = items
    .filter(x => x.active)
    .map(x => ({ id: x.id, n: x.name.length }));

  const onSave = () => {
    // 将时间戳生成放到交互时刻(仍在事件中,不是渲染)
    onSaveRaw({ ts: Date.now() });
  };

  return <Panel data={data} options={{ theme, size: 'm' }} onSave={onSave} />;
}

说明:

  • 编译器将为 data、options、onSave 在依赖未变化时提供稳定引用与缓存
  • Date.now() 发生在事件回调(不是渲染路径),不破坏纯性

13. 常见代码味道与替代方案清单

  • 在渲染里调用随机或时间
    • 替代:事件回调中调用;或 useState(() => Date.now()) 仅初始化一次
  • 在渲染里读取可变单例(例如:全局缓存、单例服务的内部可变字段)
    • 替代:通过 props/state/context 显式传入;在 Effect 中与外界同步
  • 手工 useMemo 包裹一切
    • 替代:先删掉;保留只有协议或极端热点需要的用法
  • 写“稳定引用辅助库”绕来绕去
    • 替代:交给编译器处理;必要时用 useRef/useEffectEvent

14. 具体实例

  • React 编译器让你写更“自然”的 React 代码,同时获得更少重渲染、更低 CPU 消耗与更稳定的交互性能。
  • 想要“优雅使用”的关键,是遵循“纯渲染、不可变数据、明确依赖”的基本法则,剩下交给工具。
  • 配合 ESLint 与渐进灰度,既能获得性能红利,也能维持可控的工程风险。

说明:

  • 下面的示例展示在启用 React 编译器(automatic memoization)后,如何“直写业务逻辑”而不用铺满 useMemo/useCallback/memo,并与旧方法对比。
  • 所有示例都基于“纯渲染、不可变数据、稳定引用”三原则。编译器在依赖不变时会自动稳定对象/函数/数组引用、缓存派生数据,减少不必要重渲染。
  • 仍有与外部协议交互(DOM/第三方库)等场景需要手工稳定引用或放到 Effect 中管理,示例也会给出对比。

14.1 子组件避免无谓重渲染:内联对象/函数/数组

旧方法:手工 useMemo/useCallback 保持 props 引用稳定

// 旧方法
function Parent({ items, theme }) {
  const options = useMemo(() => ({ theme, pageSize: 20 }), [theme]);
  const onSelect = useCallback((id: string) => {
    console.log('selected', id);
  }, []);
  const data = useMemo(() => items.filter(x => x.active), [items]);

  return <List data={data} options={options} onSelect={onSelect} />;
}

启用编译器后:直写,引用由编译器稳定化与缓存

// 新方法(启用编译器)
function Parent({ items, theme }) {
  const data = items.filter(x => x.active);              // 派生数据直写
  const onSelect = (id: string) => console.log('selected', id); // 回调直写

  return (
    <List
      data={data}
      options={{ theme, pageSize: 20 }} // 内联对象
      onSelect={onSelect}               // 内联函数
    />
  );
}

要点:

  • 编译器会在依赖不变时稳定 data/options/onSelect 的引用,减少子组件重渲染。
  • 代码更直白;减少误用 useMemo/useCallback 的认知负担。

14.2 昂贵派生数据(filter/sort/map):交给编译器自动缓存

旧方法:用 useMemo 缓存计算结果

// 旧方法
function Grid({ rows, query }) {
  const filtered = useMemo(() => {
    const f = rows.filter(r => r.name.includes(query));
    return f.sort((a, b) => a.name.localeCompare(b.name));
  }, [rows, query]);

  return <Table rows={filtered} />;
}

新方法:直写昂贵计算,编译器在依赖不变时缓存

// 新方法(启用编译器)
function Grid({ rows, query }) {
  const filtered = rows
    .filter(r => r.name.includes(query))
    .sort((a, b) => a.name.localeCompare(b.name));

  return <Table rows={filtered} />;
}

注意:

  • 保持不可变数据操作:如使用 .sort() 时请先复制数组 [...rows].sort(...),避免原地修改。
  • 编译器只在渲染纯净时优化;请勿在计算中引入随机数、时间等不纯数据。

14.3 列表项与 key:子项 props 更大胆内联

旧方法:为了稳定引用对子项 props useMemo/useCallback

// 旧方法
function List({ items }) {
  return (
    <ul>
      {items.map(item => {
        const props = useMemo(() => ({ active: item.active, label: item.label }), [item.active, item.label]);
        const onClick = useCallback(() => doSomething(item.id), [item.id]);
        return <Item key={item.id} {...props} onClick={onClick} />;
      })}
    </ul>
  );
}

新方法:子项 props 直写,编译器稳定引用;确保 key 正确

// 新方法(启用编译器)
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <Item
          key={item.id}
          active={item.active}
          label={item.label}
          onClick={() => doSomething(item.id)}
        />
      ))}
    </ul>
  );
}

要点:

  • key 必须稳定(如 item.id),编译器不会纠正错误的 key。
  • 直写子项 props + 回调更简洁,且在依赖不变时不触发无谓重渲染。

14.4 Context Provider 的 value:直写不变结构

旧方法:用 useMemo 保持 value 稳定

// 旧方法
function App({ theme, user }) {
  const value = useMemo(() => ({ theme, user }), [theme, user]);
  return <ThemeUserContext.Provider value={value}>{/* children */}</ThemeUserContext.Provider>;
}

新方法:直写 value,编译器稳定引用

// 新方法(启用编译器)
function App({ theme, user }) {
  return (
    <ThemeUserContext.Provider value={{ theme, user }}>
      {/* children */}
    </ThemeUserContext.Provider>
  );
}

注意:

  • 不要把易变值(例如 Date.now()Math.random())放进 context value,否则每次渲染都会变,编译器不会优化。

14.5 自定义 Hook:返回稳定结构,内部纯渲染

旧方法:Hook 内使用 useMemo 保持返回对象稳定

// 旧方法
function useSelection(items: Item[]) {
  const selected = useMemo(() => items.filter(i => i.selected), [items]);
  const count = selected.length;
  return useMemo(() => ({ selected, count }), [selected, count]);
}

新方法:直写返回对象,编译器稳定引用

// 新方法(启用编译器)
function useSelection(items: Item[]) {
  const selected = items.filter(i => i.selected);
  const count = selected.length;
  return { selected, count };
}

要点:

  • Hook 顶层计算保持纯(无副作用);副作用放到 useEffect。
  • 返回对象在依赖未变时将由编译器稳定化。

14.6 与 DOM/第三方库交互:何时仍需手工稳定或用 Effect 管理

场景:DOM 事件需要配对 add/remove,或第三方库以“引用相等”作为订阅键。

旧方法:使用 useCallbackuseRef/Effect 管理订阅

// 旧方法
function ResizeWatcher() {
  const onResize = useCallback(() => {
    console.log(window.innerWidth);
  }, []);

  useEffect(() => {
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [onResize]);

  return null;
}

新方法 A:仍用 Effect 管理订阅(推荐,与编译器共存)

// 新方法(启用编译器,依然用 Effect 管理订阅)
function ResizeWatcher() {
  const onResize = () => console.log(window.innerWidth);

  useEffect(() => {
    // 事件订阅属于副作用,放到 Effect
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [onResize]); // 如果外部协议要求“严格相同引用”,建议手工稳定 onResize
  return null;
}

新方法 B:手工稳定引用,避免依赖变动导致解绑失败

// 新方法(启用编译器 + 手工稳定,用于严格要求引用稳定的协议)
function ResizeWatcher() {
  const onResize = useCallback(() => {
    console.log(window.innerWidth);
  }, []);

  useEffect(() => {
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [onResize]);

  return null;
}

要点:

  • 编译器主要优化渲染路径;订阅/事件应在 Effect 中。
  • 如果外部系统要求“同一个函数引用”来解绑,仍需 useCallback 或其他稳定手段。

14.7 React Native 样式对象:直写 style,编译器稳定引用

旧方法:样式对象 useMemo

// 旧方法(RN)
function Btn({ primary }: { primary: boolean }) {
  const style = useMemo(() => ({
    backgroundColor: primary ? '#1677ff' : '#eee',
    padding: 12,
    borderRadius: 8,
  }), [primary]);

  return <Pressable style={style} />;
}

新方法:直写 style,编译器稳定引用

// 新方法(RN + 编译器)
function Btn({ primary }: { primary: boolean }) {
  const style = {
    backgroundColor: primary ? '#1677ff' : '#eee',
    padding: 12,
    borderRadius: 8,
  };
  return <Pressable style={style} />;
}

要点:

  • 样式对象在依赖不变时保持引用稳定,减少不必要重渲染。
  • 注意保持渲染纯净,不在渲染中读取外部可变状态。

14.8 Effect 依赖与“新引用”陷阱:如何写更安全

反例:在依赖数组内内联新对象/函数

// 反例:依赖数组含新引用,每次都变,Effect每次都重跑
useEffect(() => {
  // ...
}, [{ a: 1 }]); // 每次都是新对象

正确写法:将依赖声明为稳定来源或提升到函数体

// 正确(启用编译器或不启用都适用)
function C({ a }) {
  const cfg = { a };        // 由编译器稳定化
  useEffect(() => {
    // 使用 cfg
  }, [cfg]);                 // cfg 在 a 不变时引用稳定
}

注意:

  • 即使启用编译器,也不要在依赖数组里直接写字面量新引用。
  • 将依赖提升为渲染期常量,交给编译器稳定化,再在依赖数组里使用它。

14.9 不可变更新 vs 原地修改:编译器优化的前提

反例:原地修改导致引用不稳定、语义不纯

// 反例
function Bad({ list }: { list: number[] }) {
  list.sort();     // 原地修改
  list.push(42);   // 原地修改
  return null;
}

正确:不可变更新

// 正确
function Good({ list }: { list: number[] }) {
  const sorted = [...list].sort((a, b) => a * b); // 非破坏式
  const extended = [...sorted, 42];
  return <List data={extended} />;
}

要点:

  • 编译器对不可变数据有更高优化空间。
  • Map/Set 等可变容器不要在渲染中 mutate;需要更新时在事件或 Effect 中处理,并通过 state 触发渲染。

14.10 事件中使用时间/随机数:保持渲染纯净

反例:在渲染中使用 Date.now()/Math.random()

// 反例:渲染不纯,编译器会放弃优化
function Clock() {
  const now = Date.now();
  return <div>{now}</div>;
}

正确:在事件或 Effect 中使用时间/随机

// 正确:事件中使用,不影响渲染纯性
function ClockBtn() {
  const onClick = () => alert(`clicked at ${Date.now()}`);
  return <button onClick={onClick}>Click</button>;
}

或者仅初始化一次:

// 正确:仅初始化一次
function Seeded() {
  const [seed] = useState(() => Math.random());
  return <div>seed: {seed}</div>;
}

14.11 极端/动态访问模式:仍可能需要手工 useMemo

场景:高度动态的属性访问或不可追踪来源

旧方法:明确手工缓存或重构

// 旧方法:明确 useMemo
function Dynamic({ obj, key }: { obj: Record<string, number>, key: string }) {
  const val = useMemo(() => obj[key], [obj, key]);
  return <div>{val}</div>;
}

新方法:尝试直写,但若编译器警告“依赖不可追踪”,保留 useMemo 或重构

// 新方法(启用编译器)
function Dynamic({ obj, key }: { obj: Record<string, number>, key: string }) {
  const val = obj[key]; // 若 ESLint/编译器提示不可优化,请改回 useMemo 或重构
  return <div>{val}</div>;
}

建议:

  • 当编译器无法证明优化安全,会保守放弃并给出提示。遵循 ESLint 规则,必要时保留 useMemo。

14.12 真实页面对比:去掉机械化优化后的代码简化

旧方法:遍地 useMemo/useCallback/memo

function ProductsPage({ products, currency }) {
  const data = useMemo(() => products.map(p => ({
    id: p.id,
    label: `${p.name} (${currency})`,
    price: p.price,
  })), [products, currency]);

  const sortFn = useCallback((a, b) => a.price * b.price, []);
  const options = useMemo(() => ({ currency, pageSize: 50 }), [currency]);
  const onBuy = useCallback((id: string) => track('buy', id), []);

  return <List data={data.sort(sortFn)} options={options} onBuy={onBuy} />;
}

新方法:更直白,依赖不变时自动稳定引用与缓存

function ProductsPage({ products, currency }) {
  const data = products.map(p => ({
    id: p.id,
    label: `${p.name} (${currency})`,
    price: p.price,
  }));

  const sortFn = (a, b) => a.price * b.price;
  const onBuy = (id: string) => track('buy', id);

  return (
    <List
      data={[...data].sort(sortFn)}     // 非破坏式排序
      options={{ currency, pageSize: 50 }}
      onBuy={onBuy}
    />
  );
}

收益:

  • 组件更容易读与测;减少误用依赖数组或忘记更新依赖导致的 bug。
  • 编译器在依赖不变时稳定 data/options/onBuy/sortFn 的引用,避免子组件级联重渲染。

14.13 快速落地建议(配合示例使用)

  • 开启编译器(Next.js/React Native/Babel 插件)并启用 ESLint 推荐规则(react-hooks + 对应的编译器规则)。
  • 优先在“子组件因 props 引用不稳而频繁重渲染”“昂贵派生计算反复执行”的模块试点。
  • 将事件订阅、计时器、DOM 交互统一收敛到 useEffect;保留必要的 useCallback/useMemo 以满足外部协议对稳定引用的要求。
  • 对 Map/Set/原地修改保持警惕;所有列表操作用非破坏式写法(如 [...list].sort())。
  • 用 React DevTools Profiler 做前后对比,观察提交次数、渲染时间、重渲染数量变化。

如果你希望,我可以根据你的实际代码片段进一步给出“替换清单”:标注哪些 useMemo/useCallback 可以删、哪些需要保留,并给出每段的风险与回退方案。

15. 在 Vite + React + TypeScript 项目中启用 React 编译器的实用指南

本文面向使用 Vite 创建的 React + TypeScript 项目,介绍如何启用 React 编译器(babel-plugin-react-compiler),并给出可验证、可回退的配置方案与常见坑位处理。

15.1 前置条件检查

  • React 版本:建议 React 18 及以上
  • Vite 插件:请使用 @vitejs/plugin-react(Babel 版),不要使用 @vitejs/plugin-react-swc(SWC 版)
    • 原因:React 编译器目前以 Babel 插件形式提供。SWC 管线无法直接加载该 Babel 插件
  • TypeScript tsconfig:jsx 设置为 react-jsx(Vite React 项目默认如此)

参考 tsconfig.json(通常无需改动):

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true
  }
}

15.2 安装依赖

  • 如果项目当前用的是 @vitejs/plugin-react-swc,请先替换为 Babel 版插件
# 如使用 pnpm:
pnpm remove @vitejs/plugin-react-swc
pnpm add -D @vitejs/plugin-react babel-plugin-react-compiler

# npm:
npm uninstall @vitejs/plugin-react-swc
npm i -D @vitejs/plugin-react babel-plugin-react-compiler

# yarn:
yarn remove @vitejs/plugin-react-swc
yarn add -D @vitejs/plugin-react babel-plugin-react-compiler

可选(推荐)安装 ESLint 及编译器相关规则,帮助写出“可优化”的纯渲染代码:

pnpm add -D eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-compiler

15.3 配置 Vite 使用编译器(vite.config.ts)

在 Vite 的 React 插件中启用 Babel 插件 react-compiler。示例:

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

const enableReactCompiler = process.env.REACT_COMPILER === 'true'; // 建议用环境变量控制灰度

export default defineConfig({
  plugins: [
    react({
      // 使用 Babel 管线,并注入 React 编译器插件
      babel: {
        plugins: enableReactCompiler ? ['babel-plugin-react-compiler'] : [],
      },
      // 可选:显式传入 fast refresh 相关设置或保持默认
      // fastRefresh: true
    }),
  ],
});

使用方式:

  • 开发环境开启:REACT_COMPILER=true vite
  • 生产构建开启:REACT_COMPILER=true vite build
  • 默认关闭时,babel.plugins 为空,便于快速回退与对比测试

Windows PowerShell 示例:

$env:REACT_COMPILER="true"; npm run dev

macOS/Linux 示例:

REACT_COMPILER=true npm run dev

15.4 配置 ESLint(推荐)

开启 react-hooks 与 react-compiler 的推荐规则,可在本地与 CI 自动发现影响优化的代码味道。

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:react-compiler/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2023,
    "sourceType": "module"
  },
  "settings": {
    "react": { "version": "detect" }
  }
}

说明:

  • react-compiler 规则会提示渲染不纯、依赖不可追踪、内联新引用用于 Effect 依赖等问题
  • 保留 react-hooks 规则,确保 Hooks 使用正确

15.5 快速验证编译器是否生效

  • 简单对比:删除组件中的 useMemo/useCallback(仅用于稳定引用/避免重复计算的场景),内联对象/函数,观察是否减少无谓重渲染
  • 使用 React DevTools Profiler:
    • 录制关键交互前后对比(提交次数、渲染耗时、组件重渲染数量)
  • 人工观察:
    • 在子组件中加日志,比较 props 引用是否在依赖不变时保持稳定(例如浅比较不再触发)

示例对比(启用编译器前后):

// 旧:手工稳定引用
function Parent({ items, theme }) {
  const data = useMemo(() => items.filter(x => x.active), [items]);
  const options = useMemo(() => ({ theme, pageSize: 20 }), [theme]);
  const onSelect = useCallback((id: string) => console.log(id), []);
  return <List data={data} options={options} onSelect={onSelect} />;
}

// 新:直写,交给编译器优化
function Parent({ items, theme }) {
  const data = items.filter(x => x.active);
  const onSelect = (id: string) => console.log(id);
  return <List data={data} options={{ theme, pageSize: 20 }} onSelect={onSelect} />;
}

15.6 常见坑位与修复

  • 使用了 @vitejs/plugin-react-swc
    • 现象:即使安装了 babel-plugin-react-compiler,优化仍未生效
    • 解决:改用 @vitejs/plugin-react(Babel 版),并在 vite.config.tsbabel.plugins 中加入编译器插件
  • 在渲染中使用不纯数据(Date.now/Math.random/网络/日志/订阅)
    • 现象:ESLint 或编译器警告“渲染不纯”,自动优化被禁用
    • 解决:副作用放进 useEffect,时间/随机数放到事件或用 useState(() => initial) 做一次性初始化
  • 依赖不可追踪(动态属性访问、全局可变单例)
    • 现象:警告或优化被跳过
    • 解决:把数据纳入 props/state/context;必要时手工 useMemo 或重构
  • Effect 依赖数组直接写字面量新对象/函数
    • 现象:Effect 每次重跑
    • 解决:把依赖提升为函数体常量(由编译器稳定化),再放进依赖数组

反例与修复:

// 反例:依赖数组含新引用
useEffect(() => { /* ... */ }, [{ a: 1 }]);

// 修复:提升为常量,交给编译器稳定化
function C({ a }) {
  const cfg = { a };
  useEffect(() => { /* 使用 cfg */ }, [cfg]);
}

15.7 与 DOM/第三方库交互的边界

  • 编译器优化的是“渲染路径”;订阅/事件绑定/定时器属于副作用,应放在 useEffect
  • 如果外部协议要求“严格相同的函数引用”来解绑或缓存:
    • 仍建议使用 useCallback 或 useRef + useEffect 管理订阅

    • 示例:

      function ResizeWatcher() {
        const onResize = useCallback(() => console.log(window.innerWidth), []);
        useEffect(() => {
          window.addEventListener('resize', onResize);
          return () => window.removeEventListener('resize', onResize);
        }, [onResize]);
        return null;
      }
      

15.8 灰度发布与回退建议

  • 用环境变量控制开关(REACT_COMPILER=true/false
  • 先在关键页面或小流量路径启用,观察错误率与性能指标
  • 保留少量必要的 useMemo/useCallback 用于第三方交互的稳定性协议
  • 在 CI 中强制 ESLint 规则通过,确保可优化性

15.9 常见问答

  • 必须用 Babel 吗?
    • 在 Vite 场景下,要启用现有的 React 编译器(Babel 插件),需要使用 @vitejs/plugin-react(Babel 版)。SWC 版不支持加载 Babel 插件
  • 会影响构建时间吗?
    • 取决于项目规模。构建时可能略增开销,但运行时通常可减少多余渲染与重复计算
  • 我能删除所有 useMemo/useCallback 吗?
    • 大多数“仅为稳定引用/避免重复计算”的用法可以删除;与第三方库交互或需“只创建一次”的实例仍保留相应手段(useRef/useEffect/useState 初始化)