微信小程序 + Taro 从 ESLint 迁移到 Oxlint 让项目代码检查速度提升近 12 倍
项目里有些问题,我希望在提交代码之前就能发现。比如变量声明后没有使用,条件判断里误写了赋值,或者 useEffect 少写了依赖。这些问题不一定马上影响页面运行,但等到出现异常再排查,往往要花更多时间。
我目前用 ESLint 做这层检查。项目是一个 pnpm Monorepo,包含 Web、微信小程序和公共业务包,在根目录执行一次 pnpm lint,就能把它们一起检查。
ESLint 的基础推荐规则主要处理常见的代码错误,例如重复定义对象属性、循环方向写反、switch 意外贯穿、无效正则,以及未使用的变量。它本身不会自动覆盖所有框架和语言特性,因此我又加了 TypeScript、React Hooks 和 Fast Refresh 相关配置。
这套检查能解决我关心的不少问题,只是全量运行需要大约十秒。我觉得这个等待有点久,想在保留现有检查能力的前提下,看看能不能换一个更快的工具。
解决方案
在尝试替换之前,我先把当前配置里每一部分负责什么理清楚。只有知道原来检查了什么,才能判断 Oxlint 有没有少检查东西。
我在 ESLint 里用了哪些规则
基础部分是 @eslint/js 的推荐配置。它检查的是 JavaScript 中比较通用的问题,比如 no-debugger、no-dupe-keys、no-constant-condition 和 use-isnan。
TypeScript 部分使用 typescript-eslint 的推荐配置,处理显式 any、未使用变量、不必要的类型约束等问题。它也会关闭或替换一些基础规则,避免重复检查。比如 TS、TSX 中的未使用变量由 @typescript-eslint/no-unused-vars 检查,no-undef 则被关闭,未定义变量交给 TypeScript 检查。
需要说明的是,我没有在 ESLint 中开启依赖类型信息的规则,所以这套配置不能代替 tsc。
React 相关检查分成两部分:
| 插件 | 我主要用它检查什么 |
|---|---|
eslint-plugin-react-hooks |
Hook 调用位置、依赖数组,以及渲染纯度、ref 使用、state 更新等问题 |
eslint-plugin-react-refresh |
组件文件的导出方式是否符合 Fast Refresh 的要求 |
React Hooks 用的是 recommended-latest。项目安装的插件版本为 7.1.1,实际启用了 17 条规则:
规则,省略 react-hooks/ 前缀 |
检查内容 | 级别 |
|---|---|---|
rules-of-hooks |
Hook 调用是否符合规则 | error |
exhaustive-deps |
Effect、Memo、Callback 等依赖是否完整 | warn |
static-components |
是否在渲染过程中动态定义组件 | error |
use-memo |
useMemo 的常见错误用法 |
error |
void-use-memo |
useMemo 是否返回值、结果是否被使用 |
error |
preserve-manual-memoization |
Compiler 能否保留已有的手动记忆化行为 | error |
incompatible-library |
是否使用了与记忆化不兼容的库或 API | warn |
immutability |
是否直接修改 props、state 等不可变数据 | error |
globals |
是否在渲染期间修改全局变量 | error |
refs |
是否在渲染期间不当读写 ref | error |
set-state-in-effect |
是否在 effect 中同步更新 state,造成额外渲染 | error |
error-boundaries |
是否错误地用 try/catch 捕获子组件渲染异常 | error |
purity |
渲染期间是否调用已知的不纯函数 | error |
set-state-in-render |
是否在渲染期间不当更新 state | error |
unsupported-syntax |
是否使用 Compiler 不支持的语法 | warn |
config |
React Compiler 配置是否有效 | error |
gating |
React Compiler 条件启用配置是否有效 | error |
项目根目录的 eslint.config.js 完整配置如下:
javascript
import js from "@eslint/js"
import globals from "globals"
import reactHooks from "eslint-plugin-react-hooks"
import reactRefresh from "eslint-plugin-react-refresh"
import tseslint from "typescript-eslint"
import { defineConfig, globalIgnores } from "eslint/config"
export default defineConfig([
globalIgnores(["**/dist/**", "packages/common/src/generated/**"]),
{
files: ["**/*.{js,mjs,cjs}"],
extends: [js.configs.recommended],
languageOptions: { globals: globals.node },
},
{
files: ["apps/**/*.{ts,tsx}", "packages/**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
],
},
{
files: ["apps/**/*.{ts,tsx}"],
extends: [reactHooks.configs.flat["recommended-latest"]],
},
{
files: ["apps/web/**/*.{ts,tsx}"],
extends: [reactRefresh.configs.vite],
languageOptions: {
globals: globals.browser,
},
},
{
files: ["apps/web/src/components/ui/**/*.{ts,tsx}"],
rules: {
"react-refresh/only-export-components": "off",
},
},
])
这里忽略了构建产物和接口生成代码。Hooks 检查作用于 Web 和小程序,Fast Refresh 只检查 Web,UI 组件目录另外保留了导出规则的例外。
根目录 package.json 中的执行入口是:
json
{
"scripts": {
"lint": "eslint apps packages eslint.config.js"
}
}
348 个文件,每次检查约十秒
这次实际进入检查范围的代码是 348 个文件,不是整个仓库的文件总数,也没有把生成代码和构建产物算进去。
在 Node.js 24.15.0、ESLint 10.14.0 的环境下,我关闭 lint 缓存,直接调用 CLI,以 JSON 格式输出,连续运行五次:
| 次数 | ESLint 耗时 |
|---|---|
| 第 1 次 | 10.012 秒 |
| 第 2 次 | 10.438 秒 |
| 第 3 次 | 10.294 秒 |
| 第 4 次 | 9.871 秒 |
| 第 5 次 | 10.034 秒 |
| 中位数 | 10.034 秒 |
十秒不至于让检查没法用,但对这样一个经常执行的命令,我还是希望它能更快一点。
于是我找到了 Oxlint。它基于 Rust 的 Oxc 工具链,重点优化运行性能,同时内置了许多常用 ESLint 插件的规则实现,包括 TypeScript 和 React 相关规则。它也提供 ESLint 配置迁移工具,并支持通过 JavaScript 插件保留部分原有插件能力。Oxlint 官方介绍、迁移指南
这让我觉得可以试一试。不过,支持迁移不等于所有规则和行为都一样,我还是需要拿当前项目的配置和结果来对照。
把现有配置转换到 Oxlint
项目已经使用 ESLint Flat Config,可以从根目录的 eslint.config.js 开始转换。操作前先提交或备份现有配置,然后在项目根目录执行:
bash
pnpm add -Dw oxlint@1.93.0
pnpm dlx @oxlint/migrate@1.93.0 eslint.config.js --with-nursery --details
这里固定的是本次对比使用的版本。
--details 会列出无法迁移的规则;--with-nursery 则用来包含开发中的规则。当前配置里的 no-undef 和 no-useless-assignment 在这个 Oxlint 版本中属于这一类,不加参数会被跳过。
命令会将配置保存到根目录的 .oxlintrc.json。生成之后,我没有直接开始计时,因为迁移工具给出了部分嵌套文件匹配条件不受支持的警告。
核对配置时,还有几处需要处理:TypeScript 对基础规则的关闭、替换关系没有完整保留下来,preserve-caught-error 的部分参数也不能原样传给 Oxlint。Web 的 Fast Refresh 参数和 UI 目录例外同样需要确认。
我手动编辑了生成的 .oxlintrc.json,把这些内容尽量和原配置对齐。这里不适合拿另一份项目的配置直接覆盖,具体调整还是要以自己的迁移警告和原有规则为准。
保存 JSON 文件后,在项目根目录运行:
bash
pnpm exec oxlint -c .oxlintrc.json apps packages eslint.config.js
CLI 每次执行都会重新读取配置,不需要重启服务。我先确认它检查的也是同一批 348 个文件,再对比耗时。
如果最终决定切换日常入口,可以编辑根目录的 package.json,将 lint 改为:
json
{
"scripts": {
"lint": "oxlint apps packages eslint.config.js"
}
}
保存后,执行 pnpm lint 就会调用 Oxlint。这里保留 eslint.config.js 作为检查对象,是为了与原来的范围一致;后续如果删除这个文件,也需要从命令里移除。
Oxlint 五次都跑进了一秒以内
Oxlint 使用 1.93.0,仍然不开 lint 缓存,也没有额外开启类型感知检查。两边都直接调用 CLI、输出 JSON,统计整个进程耗时,不包含安装依赖和转换配置的时间。
| 次数 | ESLint | Oxlint |
|---|---|---|
| 第 1 次 | 10.012 秒 | 0.838 秒 |
| 第 2 次 | 10.438 秒 | 0.826 秒 |
| 第 3 次 | 10.294 秒 | 0.864 秒 |
| 第 4 次 | 9.871 秒 | 0.855 秒 |
| 第 5 次 | 10.034 秒 | 0.852 秒 |
| 中位数 | 10.034 秒 | 0.852 秒 |
按中位数计算,Oxlint 的速度约为 ESLint 的 11.8 倍 ,耗时减少 91.5% ,每次全量检查节省约 9.2 秒。
对我来说,这个收益已经足够明显。原来十秒左右的等待缩短到了不到一秒,值得继续评估迁移。当然,这只是本机、当前代码和这套规则下的结果,不能直接套到其他项目上。
原来的规则和报错能兼容多少
按当前启用的 ESLint 规则名称去重,一共有 106 条,其中 102 条在 Oxlint 中有对应实现,约占 96%。
| 规则来源 | 当前启用 | Oxlint 有对应实现 |
|---|---|---|
| ESLint 核心规则 | 68 | 66 |
| TypeScript | 20 | 20 |
| React Hooks | 17 | 15 |
| React Refresh | 1 | 1 |
| 合计 | 106 | 102 |
缺少对应项的是 no-dupe-args、no-octal、react-hooks/config 和 react-hooks/gating。
迁移工具认为,前两条涉及的语法可以由严格模式约束;后两条则与 React Compiler 的配置方式有关。再加上前面提到的开发中规则和配置参数差异,我不会把这个覆盖率理解成完全兼容。
实际代码的检查结果也需要一起看:
| 诊断结果 | ESLint | Oxlint |
|---|---|---|
| 错误 | 9 | 9 |
| 警告 | 29 | 53 |
原来的九个错误,包括五个 ref 使用问题、两个渲染纯度问题,以及两个 effect 中同步更新 state 的问题,在 Oxlint 中全部检出,规则和位置也都能对应上。
警告则都是 Hooks 依赖检查,两边的报告方式有差异。有些地方 ESLint 将多个依赖问题合并在一条警告里,Oxlint 会拆开;有些地方两边都提示缺少依赖,Oxlint 还会额外指出某些 *DataVersion 依赖多余。
所以,多出的 24 条警告不能直接算成 24 个新缺陷。我能确认的是,ESLint 原来提示的 29 个依赖警告位置,在 Oxlint 中都有对应报告。
还有一点需要留意:Oxlint 的 React Compiler 原生规则目前仍被官方标为实验性。官方插件说明
对于当前项目,我认为性能收益值得推进迁移,但是否彻底移除 ESLint,还需要结合这些差异判断。如果希望保留官方 React Hooks 插件的检查行为,也可以先让 Oxlint 接管基础 JS、TS 等规则,ESLint 继续负责 Hooks。这种混合方案本次没有测量耗时,不能直接套用 0.852 秒的结果。