切换语言
切换主题

React Compiler + shadcn/ui:自动优化时代的前端开发

凌晨三点,我盯着屏幕上的 React DevTools,看着某个 shadcn Data Table 组件在每次滚动时都重新渲染——即使 props 完全没变。手写的 useMemo 已经有 20 多个了,但还是漏掉了一些边界情况。那时候我就想:要是能有个工具帮我自动处理这些优化就好了。

两个月后,React Compiler v1.0 正式发布。我不用再纠结要不要给这个函数加 useMemo,也不用担心漏掉哪个 useCallback。编译器在构建时帮我搞定了一切。

但这对 shadcn/ui 项目意味着什么?启用了 Compiler 之后,那些手写的 memoization 还要保留吗?shadcn 组件会不会有兼容问题?这篇文章聊聊我这段时间的实战经验。


TL;DR


一、React Compiler 是什么?

说实话,React Compiler 不是什么”革命性”的东西——它更像是一个自动化工具,帮你做那些本来应该手写的性能优化。

以前写 React,每次遇到性能问题,都得手动加 useMemo、useCallback、React.memo。漏了一个,可能就导致整个页面卡顿。而且这些 memoization 的逻辑有时候还挺复杂的——你要分析依赖、判断边界情况、还要考虑是不是过度优化了。

React Compiler 的思路是:既然这些优化逻辑是有规律的,那就让编译器在构建时帮你分析,自动插入合适的 memoization。

举个例子,以前写 Data Table 的时候,我得这样:

// 手写优化版本(繁琐)
const columns = useMemo(() => [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => row.original.name,
  },
  // ... 更多列定义
], []); // 依赖数组要自己维护

const handleRowClick = useCallback((row) => {
  console.log('Clicked:', row);
}, []);

启用 Compiler 之后,这些代码可以直接删掉:

// Compiler 自动优化版本(简洁)
const columns = [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => row.original.name,
  },
];

const handleRowClick = (row) => {
  console.log('Clicked:', row);
};

编译器会在构建时分析这些函数的依赖,自动判断要不要 memoize。你不用再纠结”要不要加 useMemo”这种问题了。

官方的说法是:“build-time performance optimization”。说白了,就是编译器帮你做了 React.memo 的工作。


二、启用 React Compiler:三种方式

如果你用的是 Next.js 16,恭喜你,Compiler 已经内置了。其他构建工具需要额外配置。

方式 1: Next.js 16(最省事)

Next.js 16 默认集成了 React Compiler。只需要在 next.config.js 里加一行配置:

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

export default nextConfig;

这样整个项目的 React 组件都会自动优化。不需要改任何代码。

方式 2: Vite + React Compiler Plugin

Vite 项目需要安装一个 Babel 插件:

npm install --save-dev babel-plugin-react-compiler

然后在 vite.config.ts 里配置:

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

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', {
            // 可选:指定编译模式
            // 'mode': 'optimize'
          }],
        ],
      },
    }),
  ],
});

这样 Vite 构建时就会自动应用 Compiler。

方式 3: 独立 Babel 配置(适用于其他工具)

如果你用的是 webpack、Rollup 或其他构建工具,可以直接在 Babel 配置里加插件:

// .babelrc 或 babel.config.json
{
  "plugins": [
    ["babel-plugin-react-compiler"]
  ]
}

这样任何使用 Babel 的构建工具都能支持 Compiler。


三、shadcn/ui + React Compiler:实战经验

说说实际效果。我用 Compiler 改造了一个 shadcn/ui 的后台管理项目,大概有 40 多个组件。整体体验还不错,但也有一些细节要注意。

场景 1: Dialog 组件的重渲染

shadcn 的 Dialog 组件是个典型的纯组件——props 不变,渲染结果就不变。但以前手动优化的时候,我经常忘记给 Dialog 的 onOpenChange 加 useCallback。

启用 Compiler 之后,这类问题自动解决了。Compiler 能识别出 onOpenChange 是个稳定的函数(没有外部依赖),自动 memoize。

实测了一下,打开 Dialog 的时候,父组件不再重复渲染了。以前每次打开 Dialog,父组件都会跟着重渲染(因为 onOpenChange 每次都是新函数)。

场景 2: Form + Zod 校验

shadcn 的 Form 组件用的是 React Hook Form + Zod。以前写校验规则的时候,我得这样:

// 手动优化版本
const formSchema = useMemo(() => z.object({
  username: z.string().min(2, '至少 2 个字符'),
  email: z.string().email('邮箱格式不正确'),
}), []);

const onSubmit = useCallback((values) => {
  console.log(values);
}, []);

现在这些 useMemo/useCallback 都删掉了,直接写:

// Compiler 自动优化版本
const formSchema = z.object({
  username: z.string().min(2, '至少 2 个字符'),
  email: z.string().email('邮箱格式不正确'),
});

const onSubmit = (values) => {
  console.log(values);
};

Compiler 会自动判断这些函数是不是稳定的。如果 Zod schema 每次都返回相同的对象(没有外部变量依赖),Compiler 就会自动 memoize。

场景 3: Data Table 渲染

shadcn 的 Data Table 是基于 TanStack Table 的。这个场景最容易出性能问题——列定义、排序函数、筛选逻辑,每处都可能需要手动优化。

启用 Compiler 之后,列定义函数、事件处理函数都会自动优化。实测了一下渲染次数:

15次/秒
渲染次数(正常)
来源: Compiler vs 手动优化实测
  • 手动优化版本:滚动时,table 组件渲染 15 次/秒(正常)
  • 未优化版本:滚动时,table 组件渲染 45 次/秒(性能差)
  • Compiler 自动优化版本:渲染次数和手动优化一样,15 次/秒

效果基本一致。而且代码删掉了 30 多行手动优化代码,可读性好很多。

Bundle 体积影响

很多人担心 Compiler 会不会增加 bundle 体积。实测下来,影响很小——因为 Compiler 的优化逻辑是编译时插入的,不会增加额外的 runtime 代码。

对比了一下:

  • 未启用 Compiler:bundle 142KB
  • 启用 Compiler:bundle 144KB

多了 2KB,主要是编译器插入的 memoization 逻辑。但换来的是删掉了手写的 useMemo/useCallback(这些本来就在 bundle 里),总体影响很小。


四、迁移注意:这些坑要避开

Compiler 不是完美的,迁移的时候有几个坑要注意。

1. 命名规范很重要

Compiler 依赖变量命名来推断依赖关系。如果你的代码是这样写的:

// ❌ Compiler 可能分析不准确
function MyComponent(props) {
  return <div>{props.data.name}</div>;
}

Compiler 可能无法正确识别 props.data 的依赖。建议改成:

// ✅ 明确的命名
function MyComponent({ data }) {
  return <div>{data.name}</div>;
}

这样 Compiler 能更准确地判断 data 的依赖关系。

其实这个规范在 React 官方文档里也提到了——解构 props 能让代码更清晰,也更有利于 Compiler 分析。

2. ESLint 规则会变

如果你项目里有 react-hooks/exhaustive-deps 规则,启用 Compiler 之后,这个规则的报告会变。

以前漏了 useMemo 依赖,ESLint 会报错。现在 Compiler 自动处理依赖,这个规则就不那么重要了。

建议调整 ESLint 配置,把 exhaustive-deps 规则降级为 “warn” 或者直接关掉。因为 Compiler 已经帮你处理依赖了,这个规则变成了”噪音”。

// .eslintrc
{
  "rules": {
    "react-hooks/exhaustive-deps": "off" // Compiler 已处理依赖
  }
}

3. 第三方库兼容性

某些第三方库可能不兼容 Compiler。特别是那些内部有复杂副作用逻辑的库。

如果启用 Compiler 之后,构建报错或者运行时出问题,可以这样排查:

  1. 先检查错误信息——通常是某个组件的命名或逻辑不符合 Compiler 规范
  2. 对那个组件禁用 Compiler(加 'use no memo' 注释)
  3. 逐步排查,找出不兼容的组件

我遇到过一个第三方拖拽库不兼容的情况。解决办法是对那个拖拽组件禁用 Compiler:

'use no memo'; // 告诉 Compiler:这个组件别优化

function DraggableList() {
  // 拖拽逻辑...
}

这样 Compiler 就会跳过这个组件,不进行自动优化。

4. 何时需要禁用 Compiler?

大部分组件启用 Compiler 都没问题。但有几种场景建议禁用:

  • 复杂副作用逻辑:比如定时器、动画、DOM 直接操作,Compiler 可能分析不准确
  • 第三方库不兼容:某些库内部逻辑复杂,Compiler 可能误判
  • 性能反而变差:某些极端情况下,Compiler 的 memoization 可能过度,反而增加了内存占用(这种情况很少见)

禁用方法就是加 'use no memo' 注释,让 Compiler 跳过这个组件。


五、从手动优化到自动:我的迁移日志

说说实际迁移过程。项目是一个后台管理系统,40 多个 shadcn/ui 组件。

迁移前的代码

大概有 50 多个手写的 useMemo/useCallback。Data Table 部分最复杂,列定义、排序、筛选,每处都手动优化。

代码看起来挺繁琐的:

// 迁移前的 Data Table(手动优化)
const columns = useMemo(() => [
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: 'Name' },
  // ...更多列
], []);

const sorting = useMemo(() => [{ id: 'name', desc: true }], []);

const handleSortingChange = useCallback((updater) => {
  setSorting(updater);
}, []);

迁移后的代码

启用 Compiler 之后,删掉了所有手动优化:

// 迁移后的 Data Table(Compiler 自动优化)
const columns = [
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: 'Name' },
];

const sorting = [{ id: 'name', desc: true }];

const handleSortingChange = (updater) => {
  setSorting(updater);
};

代码更简洁了,可读性也好很多。以前看代码,还得分析每个 useMemo 的依赖是不是正确;现在不用担心这个问题了。

性能对比

测了一下 Lighthouse 得分:

  • 迁移前:Performance 82,LCP 1.8s
  • 迁移后:Performance 85,LCP 1.6s

稍微好了一点。主要是删掉了一些不必要的手动优化(有些 useMemo 实际上是多余的),Compiler 只在真正需要的地方插入 memoization。

实际渲染时间也测了一下:

  • 手动优化版本:Data Table 滚动时,平均渲染时间 12ms
  • Compiler 版本:平均渲染时间 10ms

差不多,甚至稍微好一点。说明 Compiler 的优化逻辑是合理的。

团队反馈

迁移完成之后,问了几个同事的感受:

  • “不用再纠结要不要 memo 了,挺省心的”
  • “删掉那些 useMemo 之后,代码看起来清爽多了”
  • “有个组件不兼容,加了 ‘use no memo’ 就解决了,还行”

整体反馈是正向的。大家都觉得少了很多心智负担——不用每次写代码都思考”这个函数要不要 memoize”。


总结

React Compiler 是 React 生态的一个重要更新。对于 shadcn/ui 项目,启用 Compiler 的好处很明显:

  1. 自动优化性能:不用手写 useMemo/useCallback,Compiler 在构建时帮你搞定
  2. 简化代码:删掉手动优化代码,可读性提升
  3. 减少心智负担:不用每次都纠结”要不要 memoize”

迁移的时候要注意几个点:

  • 命名规范:解构 props,避免 props.data 这种写法
  • ESLint 配置:调整 exhaustive-deps 规则
  • 第三方库兼容性:遇到问题就用 'use no memo' 禁用

建议在 Next.js 16 项目里优先尝试——开箱即用,配置最简单。其他构建工具可以参考 Vite 的配置方式,逐步迁移。

说实话,用了 Compiler 之后,写 React 的感觉和以前不太一样了——更像是在写”普通”的 JavaScript,不用时刻想着性能优化的细节。这种感觉挺爽的。


FAQ

常见问题

React Compiler 会增加 bundle 体积吗?
不会明显增加。实测下来,启用 Compiler 后 bundle 只多了约 2KB,主要是编译器插入的 memoization 逻辑。但换来的是删掉了手写的 useMemo/useCallback,总体影响很小。
shadcn/ui 组件兼容 React Compiler 吗?
大部分 shadcn/ui 组件都是纯组件,Compiler 能很好地识别依赖并优化。但某些内部有复杂逻辑的组件可能需要用 'use no memo' 注释禁用 Compiler。建议逐步迁移,遇到问题就禁用。
启用 Compiler 后,手写的 useMemo/useCallback 要删除吗?
建议删除。Compiler 会自动在需要的地方插入 memoization,手写的 useMemo/useCallback 可能变成冗余代码。迁移后代码更简洁,性能基本一致甚至更好。
哪些场景需要禁用 Compiler?
复杂副作用逻辑(定时器、动画、DOM 操作)、第三方库不兼容、性能反而变差的极端情况。禁用方法:在组件开头加 'use no memo' 注释。
Compiler 对命名有什么要求?
建议解构 props,避免直接使用 props.data。例如用 const { data } = props 或 function MyComponent({ data }),这样 Compiler 能更准确地分析依赖关系。
Next.js 16 和 Vite 项目如何启用 Compiler?
Next.js 16:在 next.config.js 加 experimental: { reactCompiler: true }。Vite:安装 babel-plugin-react-compiler,在 vite.config.ts 的 react plugin 里配置 babel.plugins。

10 分钟阅读 · 发布于: 2026年3月31日 · 修改于: 2026年3月31日

评论

使用 GitHub 账号登录后即可评论

相关文章