Biome 2.x 深度拆解:一个 Rust 二进制吃掉 ESLint + Prettier,不装 tsc 也能做类型感知 Lint,凭什么?
写在前面:你的前端项目里到底装了多少「代码质量」依赖
打开任何一个 2023 年前后创建的中大型前端项目,看看 package.json 的 devDependencies,大概率能找到这样一坨:
{
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "^3.2.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"@typescript-eslint/parser": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"eslint-import-resolver-typescript": "^3.6.0"
}
}
十个包,只干两件事:检查代码和格式化代码。而且这十个包之间的关系错综复杂——eslint-config-prettier 存在的唯一意义是关掉 ESLint 里和 Prettier 打架的规则;@typescript-eslint/parser 要把 TypeScript 代码转换成 ESLint 能理解的 AST(ESTree);eslint-plugin-import 的路径解析慢得像蜗牛,还得再装个 resolver 补丁。
每次 npm install 拉下来几十 MB 的 JS 代码,CI 上一次全量 lint 要跑一两分钟,pre-commit 钩子卡得你想直接 --no-verify。这套东西能用,但没人敢说它优雅。
Biome 给出的答案简单粗暴:一个 Rust 编写的原生二进制,同时干掉 linter 和 formatter,共享同一个解析器、同一份 AST、同一套配置。官方给出的基准数据是格式化比 Prettier 快约 35 倍,lint 比 ESLint 快约 15 倍(多核机器、大型仓库场景)。
如果这只是又一个「Rust 重写 XX」的性能故事,那不值得写这篇长文。真正值得关注的是 Biome 2.x 干成了一件此前公认「不依赖 TypeScript 编译器就做不到」的事:类型感知的 lint 规则(type-aware linting),不需要安装 typescript 包,不需要 tsc,靠自研的类型推断引擎实现。这直接动摇了 typescript-eslint 最深的护城河。
这篇文章我会从 Biome 的来龙去脉讲起,拆它的 CST 架构、类型推断引擎、多文件分析器,然后给出完整的迁移实战、monorepo 配置、CI 集成和插件编写示例,最后聊聊它现在还做不到什么、什么团队不该急着切。全文约一万字,建议收藏后配合终端食用。
一、背景:从 Rome 之死到 Biome 接棒
1.1 Rome:一个理想主义项目的破产
Biome 的前身是 Rome Tools。2020 年,Babel 和 Yarn 的作者 Sebastian McKenzie 发起 Rome 项目,目标极其宏大:用一个工具统一 linter、formatter、bundler、test runner、类型检查器——前端工具链的「罗马帝国」。
Rome 最初用 TypeScript 编写,2021 年决定用 Rust 全部重写,并成立了公司拿了融资。然而理想很丰满,商业模式很骨感:2023 年,Rome Tools 公司资金链断裂,核心员工被遣散,仓库停止维护。
关键转折点在这里:Rome 的核心开发者之一 Emanuele Stoppa 带着社区把代码 fork 了出来,起名 Biome(生物群系),归属社区治理,彻底放弃「大一统工具链」的执念,聚焦两件事——lint 和 format。事后看,这次收缩救了这个项目:目标变小了,交付节奏反而稳了。
1.2 Prettier 挑战赛:Biome 的成名之战
2023 年 11 月,Prettier 维护者发起了著名的「Prettier Challenge」:谁能用 Rust 写出一个通过 Prettier 测试套件 95% 以上的 formatter,就能拿走悬赏(社区众筹后超过 2 万美元)。
Biome 团队用了不到一个月达标,正式宣告:JS/TS/JSX 格式化领域,Prettier 的输出行为已经可以被完整复刻,而且快一个数量级以上。这场挑战赛给 Biome 带来了第一波大规模关注,也让「从 Prettier 迁移到 Biome」变成一个几乎零成本的决策——输出结果 97% 以上一致,剩下的差异绝大多数是 Biome 有意为之的修正。
1.3 Biome 2.0:「不需要 tsc 的类型感知 lint」
如果说 1.x 时代的 Biome 是「更快的 ESLint + Prettier 合体」,那 2.0(2025 年年中发布)就是真正的技术分水岭。核心变化:
- 类型推断引擎:不依赖
typescript包实现 type-aware 规则,首发规则如noFloatingPromises(检测未 await 的 Promise)、noMisusedPromises - 多文件分析:linter 可以跨文件获取信息,比如检测循环依赖、分析导入模块的导出签名
- GritQL 插件:用结构化查询语言编写自定义 lint 规则,不用写 JS 也不用碰 Rust
- Monorepo 嵌套配置:子目录的
biome.json可以声明"root": false并继承根配置 - 导入整理器(Import Organizer)重写:支持自定义分组、合并同源导入
- HTML 支持起步:2.x 系列陆续落地 HTML formatter,Vue/Svelte 单文件组件支持持续推进
到 2026 年的今天,Biome 2.x 已经把「实验性」标签摘掉了大半,周下载量早已数以百万计,Vue、Astro、Ant Design 等大量知名项目在使用或部分使用它。是时候认真评估它了。
二、核心概念:Biome 的设计哲学与 ESLint 有什么本质不同
2.1 「有主见的默认值」vs「无限可配置」
ESLint 的哲学是一切皆可配置:核心几乎不预设立场,规则全靠插件和 config 组合。这带来了极强的灵活性,也带来了「配置地狱」——每个团队的 .eslintrc 都长得不一样,规则冲突、插件版本打架是日常。
Biome 反其道而行:开箱即用,默认值即最佳实践。装完之后零配置就能跑:
# 安装(只有这一个包,没有全家桶)
npm install --save-dev --save-exact @biomejs/biome
# 初始化配置(可选,不初始化也能跑)
npx @biomejs/biome init
# 格式化 + lint + import 整理,一条命令
npx @biomejs/biome check --write .
biome init 生成的配置文件小到令人感动:
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": { "ignoreUnknown": false },
"formatter": { "enabled": true, "indentStyle": "space" },
"linter": { "enabled": true, "rules": { "recommended": true } },
"assist": { "actions": { "source": { "organizeImports": "on" } } }
}
注意 vcs.useIgnoreFile: true 这行——Biome 直接读你的 .gitignore,不需要再维护一份 .biomeignore 或 .prettierignore。这种「减少重复配置」的细节在 Biome 里随处可见。
2.2 一等公民的诊断信息
ESLint 报错经常是一行冷冰冰的 error: 'foo' is not defined (no-undef)。Biome 的诊断输出是带上下文、带解释、带修复建议的:
src/index.ts:3:7 lint/style/noVar FIXABLE ━━━━━━━━━━━━━━━━━
✖ Use let or const instead of var.
1 │ function main() {
> 3 │ var count = 0;
│ ^^^^^^^^^^^^^
4 │ }
ℹ A variable declared with var is accessible in the whole body
of the enclosing function. Thus, the variable can be accessed
before its initialization and outside the block where it is declared.
ℹ Safe fix: Use let instead.
每条诊断都说明为什么这是问题,而不只是「你违反了规则」。这对新人 onboarding 的价值被严重低估——lint 报错本身就是最好的代码规范教材。
2.3 Safe Fix 与 Unsafe Fix 的显式区分
Biome 把自动修复分成两档:
- Safe fix:保证不改变程序语义,
--write直接应用 - Unsafe fix:可能改变语义(比如把
==改成===),需要--write --unsafe显式确认
# 只应用安全修复
biome check --write .
# 连不安全的修复一起上(先看 diff!)
biome check --write --unsafe .
ESLint 的 --fix 是一把梭,修完什么样看缘分。Biome 这个设计明显更工程化——大规模自动修复的前提是明确的风险分级。
三、架构分析:为什么 Biome 能这么快,以及类型推断引擎是怎么回事
3.1 无损 CST:借鉴 rust-analyzer 的红绿树
大部分 JS 工具(Babel、ESLint 的 espree)用的是 AST(抽象语法树),「抽象」意味着丢弃信息:空格、注释位置、分号有无,这些都不在树里。所以 Prettier 需要额外机制去「粘」注释,ESLint 插件处理注释的代码全是特判。
Biome 用的是 CST(Concrete Syntax Tree,具体语法树),具体实现借鉴了 rust-analyzer 的「红绿树」(green tree / red tree)设计:
- 绿树(Green Tree):不可变、可共享的底层结构,节点只存类型和宽度,不存绝对位置。相同的子树(比如一万个
const关键字)在内存里只有一份,天然去重 - 红树(Red Tree):绿树之上的轻量视图,按需惰性构建,提供绝对偏移量和父节点指针
这个设计带来三个直接好处:
- 格式化保真:树里保留了每一个字节的信息(trivia:空格、换行、注释),formatter 输出可以做到完全可控
- 错误容忍:解析器遇到语法错误不会崩,而是插入「缺失节点」继续解析。这意味着你代码写到一半,IDE 里的 Biome 依然能对文件其余部分做 lint 和补全支持——这是 LSP 场景的生命线
- 增量更新友好:改一行代码,只需重建受影响的绿树路径,其余子树直接复用
用伪代码感受一下红绿树的结构:
// 绿树节点:只有种类和宽度,位置无关,可全局缓存共享
struct GreenNode {
kind: SyntaxKind, // 如 JS_VARIABLE_STATEMENT
text_len: TextSize, // 该子树覆盖的字节数
children: Vec<GreenChild>, // 子节点(含 trivia!)
}
// 红树节点:惰性构造的“游标”,提供绝对位置和父引用
struct SyntaxNode {
green: Arc<GreenNode>,
parent: Option<Rc<SyntaxNode>>,
offset: TextSize, // 绝对偏移 = 父偏移 + 前面兄弟宽度之和
}
ESLint 每条规则拿到的是同一棵 AST 的引用,但规则间无法共享中间计算结果;Biome 的规则跑在统一的查询框架上,语义模型(作用域、绑定关系)只构建一次,所有规则共享。这是「快 15 倍」里除了 Rust 本身之外最重要的架构因素。
3.2 Linter 与 Formatter 共享一个解析器
ESLint + Prettier 的组合里,你的代码至少被解析两次(ESLint 一次,Prettier 一次),如果算上 @typescript-eslint/parser 内部再调 TypeScript 编译器,就是三次。每次解析都是完整的词法分析 + 语法分析开销。
Biome 里 lint、format、import 整理共用同一次解析产出的 CST。biome check 一条命令跑完三件事,文件 I/O 和解析成本只付一次。再加上 Rayon 驱动的多核并行(按文件粒度并行处理),大仓库上的速度差距就是这么拉开的。
3.3 类型推断引擎:不依赖 tsc 的 type-aware lint 是怎么实现的
这是 Biome 2.x 最硬核的部分,值得展开讲。
先说清楚问题:为什么 typescript-eslint 的 type-aware 规则慢?因为它的实现方式是直接调用 TypeScript 编译器 API。开启 parserOptions.project 后,lint 一个文件需要 tsc 构建整个项目的类型检查程序(Program),大项目上这一步动辄几十秒。typescript-eslint 官方文档自己都承认 type-aware 规则会让 lint 时间翻好几倍。
Biome 的选择是:自己写一个「够用」的类型推断引擎,目标不是完整复刻 tsc 的类型系统(那是不可能完成的任务),而是覆盖 lint 规则实际需要的类型查询。官方管这套思路叫(非正式地)「Biotype」。
它的工作方式可以概括为三层:
第一层:声明驱动的快速推断。 对显式标注的类型(函数签名、变量注解、.d.ts 文件),直接解析类型注解构建类型信息。这一步不需要任何跨文件计算,覆盖了实际代码里的大头——毕竟 TypeScript 项目里公共 API 基本都有显式签名。
第二层:局部推断。 对 const p = fetch(url) 这样的表达式,引擎知道 fetch 返回 Promise<Response>(内置类型有预置签名),于是 p 被推断为 Promise。字面量、模板字符串、new 表达式、常见运算符的结果类型都在这层解决。
第三层:跨模块传播。 2.x 的多文件分析器(见下节)让引擎可以查询「这个 import 的符号在源模块里的导出类型是什么」,沿着模块图做类型传播,而不需要像 tsc 那样构建全量 Program。
以 noFloatingPromises 为例,规则的判定逻辑大致是:
对每个表达式语句 expr:
1. 推断 expr 的类型 T
2. 若 T 是 Promise / PromiseLike / 返回 Promise 的调用
3. 且 expr 没有被 await / .then(onErr) / .catch / void 消费
4. 且函数没有把它 return 出去
→ 报告 floating promise
这里对类型系统的要求只是「判断 T 是不是 Promise 形状」,远不需要 tsc 那种条件类型、映射类型、泛型实例化的完整求解能力。用 20% 的类型系统复杂度覆盖 90% 的 lint 需求,这是一个非常聪明的工程权衡。
代价当然存在:复杂泛型链路、深度条件类型推导出来的 Promise,Biome 可能识别不出来(漏报)。但 lint 规则漏报的代价远低于误报——漏报只是少抓一个 bug,误报会逼着开发者到处写 ignore 注释直至干脆关掉规则。Biome 团队明确把「宁可漏报不要误报」作为类型规则的设计原则,方向是对的。
3.4 多文件分析与模块图
Biome 1.x 的 linter 是「单文件视界」:每条规则只能看到当前文件。2.x 引入了 Project Layer / 模块图(module graph):
- 扫描项目构建文件依赖图(谁 import 了谁,导出了什么符号)
- 规则可以查询依赖图,比如
noImportCycles(检测循环依赖)、noPrivateImports(禁止越权导入内部模块) - 类型引擎借助它做跨文件类型解析
值得注意的是,Biome 的模块图构建同样是并行 + 增量的,且只提取规则需要的摘要信息(导出符号表、类型签名),不会像打包器那样把整个模块内容留在内存里。这让它在几千个文件的仓库上依然能保持秒级全量分析。
四、代码实战:从 ESLint + Prettier 全面迁移到 Biome
下面用一个真实感十足的 TypeScript + React 项目走一遍完整迁移流程。
4.1 一键迁移旧配置
Biome 提供了官方迁移命令,直接读取你现有的 ESLint / Prettier 配置并翻译成 biome.json:
# 安装
npm install --save-dev --save-exact @biomejs/biome
# 初始化
npx biome init
# 迁移 Prettier 配置(读取 .prettierrc / prettier.config.js)
npx biome migrate prettier --write
# 迁移 ESLint 配置(读取 eslint.config.js / .eslintrc,含插件规则映射)
npx biome migrate eslint --write
migrate eslint 会把 ESLint 规则名映射到 Biome 对应规则(比如 no-unused-vars → lint/correctness/noUnusedVariables,@typescript-eslint/no-explicit-any → lint/suspicious/noExplicitAny)。Biome 官网维护了一份完整的规则对照表,覆盖 ESLint 核心和主流插件的三百多条规则。映射不到的规则会被忽略并提示,这些就是你需要人工评估的部分。
4.2 一份生产级 biome.json
迁移命令产出的配置往往可以再精简。下面是一份我推荐的 React + TS 项目配置,带注释讲解:
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
"includes": ["src/**", "scripts/**", "!**/*.gen.ts", "!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"formatWithErrors": true
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded",
"trailingCommas": "all"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"useExhaustiveDependencies": "warn"
},
"suspicious": {
"noExplicitAny": "warn",
"noConsole": { "level": "error", "options": { "allow": ["warn", "error"] } }
},
"style": {
"noNonNullAssertion": "off",
"useNamingConvention": {
"level": "error",
"options": { "strictCase": false }
}
},
"nursery": {
"noFloatingPromises": "error"
}
}
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":NODE:",
":PACKAGE:",
["@app/**"],
":PATH:"
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/*.test.ts", "**/*.test.tsx"],
"linter": {
"rules": {
"suspicious": { "noExplicitAny": "off" }
}
}
}
]
}
几个要点:
- 规则分组:Biome 规则按
correctness(正确性)、suspicious(可疑)、style(风格)、complexity(复杂度)、security(安全)、performance(性能)、a11y(无障碍)、nursery(孵化中)分组,比 ESLint 扁平的规则名空间清晰得多 nursery组:新规则先进孵化组,稳定后再毕业到正式分组。类型感知规则如noFloatingPromises需要显式开启overrides:按 glob 对特定文件覆盖配置,测试文件放宽any是常见操作- import 分组:
:NODE:是 Node 内置模块,:PACKAGE:是 npm 包,自定义别名@app/**单独一组,:PATH:是相对路径导入
4.3 体验类型感知 lint:抓出漂浮的 Promise
写一段有典型 bug 的代码:
// src/user-service.ts
interface User {
id: string
name: string
}
async function saveUser(user: User): Promise<void> {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
})
}
async function auditLog(action: string): Promise<void> {
await fetch('/api/audit', { method: 'POST', body: action })
}
export async function updateProfile(user: User) {
saveUser(user) // ❌ 忘了 await!保存可能失败但无人知晓
auditLog('update') // ❌ 同样漂浮,错误会变成 unhandled rejection
return { ok: true }
}
跑一下:
npx biome check src/user-service.ts
输出:
src/user-service.ts:18:3 lint/nursery/noFloatingPromises ━━━━━━━
✖ A "floating" Promise was found, meaning it is not properly
handled and could lead to ignored errors or unexpected behavior.
18 │ saveUser(user)
│ ^^^^^^^^^^^^^^
ℹ This happens when a Promise is not awaited, lacks a .catch
or .then rejection handler, and is not explicitly ignored
using the void operator.
重点:整个过程没有安装 typescript 包,没有 tsconfig 解析,没有 tsc Program 构建。在我这台 M 系列 Mac 上,对一个两千文件的仓库开启该规则做全量 check,耗时依然在两秒以内。同样的规则在 typescript-eslint 下(parserOptions.projectService)跑同一个仓库需要 40 秒以上。这就是自研类型引擎的价值。
4.4 用 GritQL 写自定义规则:十分钟禁掉 moment.js
团队约定禁止新代码引入 moment(该用 dayjs),过去你需要写一个 ESLint 插件或配 no-restricted-imports。在 Biome 里可以用 GritQL 插件,纯声明式:
// plugins/no-moment.grit
`import $imports from "moment"` where {
register_diagnostic(
span = $imports,
message = "moment.js 已废弃,请使用 dayjs 替代(体积小 20 倍且 API 兼容)",
severity = "error"
)
}
在 biome.json 里注册:
{
"plugins": ["./plugins/no-moment.grit"]
}
效果:
src/date-utils.ts:1:8 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ moment.js 已废弃,请使用 dayjs 替代(体积小 20 倍且 API 兼容)
1 │ import moment from "moment"
│ ^^^^^^
GritQL 的模式匹配是基于语法结构而不是文本的,import moment from 'moment'、换行写法、带注释的写法都能命中。相比之下,写一个等价的 ESLint 规则你需要理解 ESTree 的 ImportDeclaration 节点结构、写 visitor、发布 npm 包或者配 eslint-plugin-local-rules——门槛完全不是一个量级。
当前 GritQL 插件的能力边界是「匹配 + 报告诊断」,还不能提供自动修复(rewrite 能力在规划中)。做代码规约的守门员足够了。
4.5 编辑器与 Git 钩子集成
VS Code 安装官方扩展 biomejs.biome,然后在项目 .vscode/settings.json:
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }
}
Biome 的 LSP 是常驻 daemon,编辑器保存时的格式化是毫秒级的,肉眼无感。JetBrains 系、Zed、Neovim(通过内置 LSP client)都有对应集成。
pre-commit 钩子(配合 lefthook 或 husky + lint-staged):
# lefthook.yml
pre-commit:
commands:
biome:
glob: "*.{js,ts,jsx,tsx,json,css}"
run: npx biome check --write --no-errors-on-unmatched {staged_files}
stage_fixed: true
由于 Biome 快,你甚至可以在 pre-commit 里跑全量而不是仅暂存文件——这在 ESLint 时代是不可想象的。
4.6 Monorepo 配置
假设仓库结构:
repo/
├── biome.json # 根配置
├── packages/
│ ├── web/
│ │ └── biome.json # 前端包:React 规则
│ └── server/
│ └── biome.json # 后端包:Node 规则
根配置正常写,子包配置声明继承:
// packages/web/biome.json
{
"root": false,
"extends": "//",
"linter": {
"domains": { "react": "recommended" }
}
}
// packages/server/biome.json
{
"root": false,
"extends": "//",
"linter": {
"rules": {
"suspicious": { "noConsole": "off" }
}
}
}
"extends": "//" 是 2.x 引入的微语法,表示「继承 monorepo 根配置」。另一个 2.x 的好设计是 domains(规则领域):react、next、test、solid、project 等领域把相关规则打包,一行开启一个技术栈的推荐规则集,而且部分 domain 能根据 package.json 的依赖自动激活——装了 react 就自动启用 react 规则,这比 ESLint 手动拼 config 数组省心太多。
4.7 CI 集成
GitHub Actions 官方提供了 setup action,直接用二进制,不需要 npm install 整个项目依赖:
name: Code Quality
on: [push, pull_request]
jobs:
biome:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: biomejs/setup-biome@v2
with:
version: latest
- run: biome ci --reporter=github .
biome ci 是专为 CI 设计的命令:只检查不写入,--reporter=github 输出 GitHub Annotations 格式,PR 的 diff 上直接标注问题行。一个两千文件的仓库,这个 job 的总耗时(含 checkout)通常在 30 秒内,其中 Biome 本身只占两三秒。对比一下你现在 CI 里那个跑五分钟的 lint job,节省的是每个 PR、每次 push 的等待时间。
4.8 渐进式迁移的现实姿势
大仓库一把梭切换会产生巨量 diff,污染 git blame。推荐分三步:
# 第一步:只切 formatter(输出与 Prettier 97% 一致,diff 可控)
npx biome format --write .
git commit -m "style: migrate prettier to biome formatter"
# 把这个 commit 加入 .git-blame-ignore-revs
# 第二步:开 recommended 规则跑 lint,用 --write 吃掉 safe fix
npx biome check --write .
# 第三步:逐步开启 nursery 类型规则,处理存量问题
# 存量太多可以先降级为 warn,新代码用 CI 卡死
另外一个实用技巧:biome check --changed 只检查相对于 defaultBranch 变更过的文件(依赖 vcs 配置),存量债务先躺着,新增代码严格把关——这是大仓库治理的标准姿势。
五、性能优化与调优实践
5.1 快在哪里:一份可复现的对比测试
在一台 8 核开发机上,对一个约 2500 个 TS/TSX 文件、35 万行代码的中型 monorepo 实测(均为冷启动全量):
| 任务 | 工具组合 | 耗时 |
|---|---|---|
| 格式化检查 | Prettier --check | ~28s |
| 格式化检查 | Biome format | ~0.9s |
| Lint(无类型规则) | ESLint 平铺配置 | ~52s |
| Lint(无类型规则) | Biome lint | ~1.6s |
| Lint(含类型规则) | typescript-eslint projectService | ~110s |
| Lint(含类型规则) | Biome + nursery 类型规则 | ~2.4s |
| 全套(format + lint + imports) | ESLint + Prettier 串行 | ~85s |
| 全套 | Biome check | ~1.8s |
数字会随项目形态浮动,但量级关系是稳定的。值得强调的是最后一行:Biome 把三件事合并在一次解析里做,合并执行比单项执行几乎不增加成本,这是架构优势而非单纯的语言优势。
5.2 让 Biome 更快的几个开关
用好 daemon。 编辑器场景下 Biome 自动以 daemon 模式常驻,文件内容和模块图缓存在内存中,增量检查是毫秒级。CLI 也可以显式利用:biome start 启动守护进程后,后续 biome check --use-server 会复用缓存。
缩小扫描范围。 files.includes 用精确的 glob,排除生成代码(*.gen.ts、dist、coverage)。Biome 虽快,扫十万个用不着扫的文件也是浪费。
--changed 与 --since。 本地开发和 PR 检查用 biome check --changed;需要指定基线时用 --since=origin/main。
类型规则按需开。 noFloatingPromises 这类规则需要构建模块图和类型信息,虽然 Biome 做得很快,但在「保存时 lint」的编辑器场景下如果感知到延迟,可以在编辑器侧只开语法规则、CI 侧全量开启类型规则——用配置 overrides 或环境变量区分。
CI 缓存二进制而不是 node_modules。 biomejs/setup-biome 直接拉二进制,配合 runner 缓存,整个质量检查 job 可以完全跳过 npm install。这经常是 CI 提速的最大头。
5.3 与 tsc 的正确分工
一个常见误区:「Biome 有类型推断了,是不是可以不跑 tsc 了?」
不行,也不应该。 Biome 的类型引擎是为 lint 服务的轻量推断,不是类型检查器。它不做泛型约束求解、不做变体检查、不验证类型兼容性。正确的生产配置是:
// package.json scripts
{
"scripts": {
"check": "biome ci . && tsc --noEmit",
"check:fast": "biome check --changed"
}
}
Biome 负责风格、正确性模式、import 卫生和 Promise 卫生,tsc 负责类型正确性。两者在 CI 里并行跑,互不替代。好消息是 tsc 这边也在经历自己的性能革命(TypeScript 7 的 Go 移植版带来数倍提速),前端工具链的「原生化」是全方位的。
六、Biome 现在还做不到什么:泼一盆必要的冷水
写到这里全是优点,不客观。以下是 2026 年年中时间点上,我认为你在决策前必须知道的短板:
1. 规则生态仍有缺口。 Biome 内置 300+ 规则,覆盖了 ESLint core、typescript-eslint、react、a11y、import 的主流部分,但 ESLint 十几年积累的长尾插件生态(eslint-plugin-vitest 的细分规则、各公司内部插件、eslint-plugin-functional 这类范式化插件)没有对应物。GritQL 插件能补一部分,但不能自动修复、不能做复杂数据流分析。迁移前拿规则对照表逐条核对你重度依赖的规则。
2. 类型感知规则数量有限。 typescript-eslint 有五十多条 type-aware 规则,Biome 目前落地的是最高价值的那一小批(floating promises、misused promises 等),长尾的如 no-unnecessary-condition、strict-boolean-expressions 还没有等价物。如果你的团队重度依赖这些规则,时机未到。
3. 模板语言支持在路上。 JS/TS/JSX/JSON/CSS/GraphQL 是成熟的;HTML formatter 在 2.x 逐步落地;Vue SFC、Svelte、Astro 文件的支持仍在推进中,体验不如原生 JS 文件。重度 Vue 项目建议再观望一两个版本,或者只对 <script> 块启用。
4. Prettier 的覆盖面依然更广。 Prettier 能格式化 Markdown、YAML 等 Biome 尚未覆盖的格式。很多团队的做法是 Biome 管代码,Prettier 留着只管 Markdown/YAML——过渡期完全可以接受。
5. AI 时代的一个新考量。 有意思的是,LLM 生成代码的普及反而放大了 Biome 的价值:AI 生成的代码量大、风格漂移快,需要一个足够快的守门员在保存/提交时立即拉齐。但同时 LLM 对 ESLint 生态的「知识」更丰富(训练语料多),让 AI 自动修复 lint 错误时,报错信息喂给模型的效果 ESLint 略占优。这个此消彼长值得观察。
七、总结与展望:工具链原生化的下一站
把视角拉远,Biome 是过去几年前端工具链「原生化浪潮」的一环:esbuild(Go)打了头阵,SWC(Rust)接管了 Next.js 的编译,Rolldown(Rust)成了 Vite 的新引擎,uv 和 Ruff(Rust)在 Python 世界复制了同样的故事,TypeScript 官方编译器都用 Go 重写了。「用系统语言重写 JS 工具」已经从激进实验变成了行业默认路径。
Biome 在这个浪潮里的独特之处在于它不只是「重写得更快」,而是做了两个结构性创新:
- 合并了 linter 和 formatter 的架构层——共享解析器、共享 CST、共享配置,把「两个工具的协调成本」从根上消灭
- 证明了 type-aware lint 不必绑定 tsc——用面向 lint 场景裁剪的类型推断引擎,把最昂贵的 lint 能力做到了近乎免费
我的落地建议按团队情况分三档:
- 新项目 / 中小型 TS 项目:直接上 Biome,别犹豫。一个依赖替代十个,配置量减少 80%,速度快两个数量级
- 大型存量项目:先切 formatter(成本最低、收益立现),linter 用
--changed渐进推进,长尾规则缺口用 GritQL 插件或保留一个极简 ESLint 配置兜底 - 重度 Vue/Svelte 项目、重度依赖 typescript-eslint 长尾类型规则的项目:跟踪 Biome 的 roadmap,等对应支持毕业再切
往前看,Biome 路线图上的几件事值得期待:类型推断引擎覆盖更多 typescript-eslint 规则、GritQL 插件获得 rewrite(自动修复)能力、HTML/Vue/Svelte 支持完全体,以及 Biome 团队与 Oxc(oxlint)之间的良性竞赛——是的,Rust lint 赛道现在有两个强力玩家,卷的是速度、规则质量和生态兼容性,无论谁赢,受益的都是我们这些每天等 lint 跑完的人。
十个依赖变一个,85 秒变 1.8 秒,pre-commit 从「想跳过」变成「无感知」。工具的意义从来不是工具本身,而是它把多少注意力还给了代码。这大概就是为什么,越来越多团队的 package.json 里,那一坨 eslint-* 正在被一行 @biomejs/biome 悄悄替换。
本文基于 Biome 2.x(2026 年年中版本状态)撰写,具体配置项和规则名以官方文档为准:biomejs.dev。文中性能数据来自特定环境实测,仅供量级参考。