Biome 深度拆解:一个 Rust 工具链如何终结 JavaScript 十年之痛——从 Rome 遗产到 500 条 Lint 规则的架构哲学
背景:JavaScript 工具链的十年之痛
如果你是一个前端开发者,你的项目里大概率同时装着 ESLint、Prettier、stylelint、import-sort、editorconfig-checker……甚至更多。每个工具有自己的配置文件、自己的插件生态、自己的运行时开销。一个中等规模的 React 项目,光工具链的 node_modules 就能占掉 200MB+,每次 eslint --fix 跑一遍要 30 秒,Prettier 再跑一遍又是 10 秒。
这不是个别现象——这是 JavaScript 生态的系统性问题。
2023 年 8 月,一个名为 Biome 的项目横空出世。它的前身是 Meta 孵化的 Rome Tools,由 Sebastian McKenzie(Babel 创始人)在 2020 年创建,目标是用 Rust 重写整个前端工具链。Rome Tools Inc. 在 2023 年关闭后,核心团队 fork 了 Rome,创建了 Biome——名字取自 "Bis"(拉丁语"二")+ "Rome",意为"第二个 Rome"。
三年后的 2026 年 6 月,Biome v2.5 发布,拥有 500 条 Lint 规则,内置格式化器、导入组织器、HTML 无障碍检查、跨语言 CSS 类检查,以及一个不需要 TypeScript 编译器的类型推断引擎。这不是一个"更好的 Prettier"——这是一个从底层重新设计的 JavaScript 工具链。
本文将从架构层面深度拆解 Biome 的每一个核心组件,带你看懂一个 Rust 项目如何用 CST(具体语法树)、增量格式化、跨文件分析和插件系统终结 JavaScript 工具链的十年之痛。
第一层:CST——一切的基石
为什么不是 AST?
传统的 JavaScript 解析器(如 Babel、TypeScript Compiler)产出的是 AST(抽象语法树)。AST 只保留语法的语义结构,丢弃了格式信息——空格、缩进、注释位置、换行符全部消失。这意味着基于 AST 的格式化器必须从零重建输出,而基于 AST 的 Lint 规则无法感知格式上下文。
Biome(继承自 Rome)选择了完全不同的路径:CST(Concrete Syntax Tree,具体语法树)。CST 保留了源代码的所有信息,包括:
- 每个 token 的精确位置(行、列、字节偏移)
- 所有空白符和注释(作为 "trivia" 附着在节点上)
- 完整的语法结构
// Biome 内部的 CST 节点定义(简化版)
pub struct JsVariableDeclarator {
id: JsBindingIdentifier,
initializer: Option<JsInitializerClause>,
// trivia(空白和注释)附着在 token 上,不在节点里
}
pub struct JsCallExpression {
callee: AnyJsExpression,
type_args: Option<TsTypeArguments>,
arguments: JsCallArguments,
}
Green Tree 与 Red Tree:Rust 的零拷贝魔法
Biome 的 CST 实现采用经典的 Green/Red Tree 模式(源自 IntelliJ IDEA 的 PSI):
Green Tree(不可变语法树):
- 不可变、可共享、按需创建
- 存储所有 token 和 trivia
- 可以通过
rowancrate 进行高效的序列化和反序列化 - 一旦创建就不会改变
Red Tree(可变语法树):
- 基于 Green Tree 构建的轻量级视图
- 支持 mutation(修改)
- 用于格式化和 Lint 修复时的代码修改
- 不存储数据,只是一个指针层
// 使用 rowan crate 构建 CST
use rowan::{GreenNode, GreenToken, SyntaxKind, Language};
// Green node 是不可变的
let green_node = GreenNode::new(
SyntaxKind::JS_VARIABLE_DECLARATION,
vec![
// 关键字 token
GreenToken::new(SyntaxKind::LET_KW, "let"),
// 空白 trivia
GreenToken::new(SyntaxKind::WHITESPACE, " "),
// 声明符节点
/* ... */
].into(),
);
// Red tree 是 green tree 的可变视图
let red_node = RedNode::new_root(green_node);
这种设计的核心优势是 增量处理:当用户在编辑器中修改代码时,Biome 不需要重新解析整个文件——它只需要修改受影响的 Green Tree 片段,然后基于新的 Red Tree 视图运行 Lint 和格式化。
从源代码到 CST:解析器的工作流
Biome 的 JavaScript/TypeScript 解析器是一个 递归下降解析器(Recursive Descent Parser),不依赖任何外部 parser(不需要 Babel、不需要 TypeScript Compiler、不需要 acorn)。这意味着:
- 零依赖:不需要安装
typescript包 - 极速启动:解析一个 1000 行的文件只需 ~2ms
- 容错解析:即使源代码有语法错误,也能产出完整的 CST
# 对比:TypeScript 编译器解析一个文件的时间
$ time tsc --noEmit --pretty false src/app.ts
real 0m3.215s # 3.2 秒
# Biome 解析同一个文件
$ time biome check src/app.ts
real 0m0.089s # 89 毫秒(36 倍快)
第二层:格式化器——增量、确定性、可预测
格式化算法的核心思想
Biome 的格式化器不依赖 CST 的完整重建。它的核心思想是:遍历 CST 的每个节点,根据预定义的格式化规则输出格式化后的代码,保留原有的 trivia(空白和注释)。
// 格式化器的核心 trait
pub trait FormatNode {
fn fmt_fields(
&self,
f: &mut JsFormatter,
) -> FormatResult<()> {
// 默认实现:格式化所有子节点
let mut elements = vec![];
// 格式化关键字
elements.push(self.keyword().format());
// 格式化标识符
elements.push(self.identifier().format());
// 格式化初始化器(如果有)
if let Some(init) = self.initializer() {
elements.push(Text!(" = "));
elements.push(init.format());
}
write_elements!(f, elements)
}
}
Prettier 兼容性:97% 的覆盖率
Biome 格式化器的设计目标是与 Prettier 保持 97% 的兼容率。这不仅仅是"输出一样的格式"——而是"对同一段代码产出与 Prettier 完全相同的输出"。
为了实现这一点,Biome 内部维护了一个 Prettier 兼容性测试套件,包含 10,000+ 个测试用例:
# 运行 Prettier 兼容性测试
$ cargo test -p biome_js_formatter -- prettier
# 结果
test result: ok. 9,847 passed; 53 failed; 0 ignored
# 兼容率:97.4%
增量格式化:只改需要改的
当用户在编辑器中触发"保存时格式化"时,Biome 不会重新格式化整个文件。它会:
- 对比当前 CST 和上一次的 CST
- 找到变化的节点
- 只重新格式化变化的部分
这使得 Biome 在大型项目中的格式化速度远超 Prettier:
# 对比格式化速度(10,000 个文件)
$ time prettier --write "src/**/*.{ts,tsx,js,jsx}"
real 1m47.32s # 107 秒
$ time biome format --write "src/**/*.{ts,tsx,js,jsx}"
real 0m3.215s # 3.2 秒(33 倍快)
第三层:Lint 引擎——从单文件到跨文件
Lint 规则的分层架构
Biome v2.5 的 500 条 Lint 规则被组织成四个域(domain):
| 域 | 规则数 | 说明 |
|---|---|---|
suspicious | ~150 | 可疑代码,可能有 bug |
correctness | ~180 | 正确性问题,几乎一定是 bug |
nursery | ~80 | 实验性规则,尚未稳定 |
a11y | ~30 | 无障碍访问问题 |
security | ~20 | 安全问题 |
style | ~40 | 代码风格建议 |
complexity | ~20 | 复杂度问题 |
performance | ~10 | 性能问题 |
// biome.json - 配置 Lint 规则
{
"linter": {
"enabled": true,
"rules": {
"recommended": true, // 推荐规则集
"correctness": {
"noUnusedImports": "error" // 标记为错误
},
"suspicious": {
"noExplicitAny": "warn" // 标记为警告
}
}
}
}
类型感知 Lint:不需要 TypeScript 的类型推断
这是 Biome v2.0 最核心的创新。传统的类型感知 Lint 规则(如 @typescript-eslint/no-floating-promises)需要完整的 TypeScript 编译器来解析类型信息,这意味着:
- 必须安装
typescript包 - 必须配置
tsconfig.json - 每次运行都要执行完整的类型检查
Biome 实现了一个 轻量级类型推断引擎,不需要 TypeScript 编译器:
// Biome 的类型推断引擎(简化逻辑)
pub struct TypeInferenceEngine {
module_graph: ModuleGraph, // 模块图
type_store: TypeStore, // 类型存储
}
impl TypeInferenceEngine {
// 推断表达式的类型
pub fn infer_type(&self, expr: &JsExpression) -> Option<Type> {
match expr {
// 字面量类型推断
JsExpression::StringLiteral(s) => Some(Type::String),
JsExpression::NumberLiteral(n) => Some(Type::Number),
JsExpression::BooleanLiteral(b) => Some(Type::Boolean),
// 标识符类型推断(需要查看声明)
JsExpression::Identifier(id) => {
self.type_store.get_type_for_binding(id)
}
// 调用表达式类型推断(需要查看函数签名)
JsExpression::CallExpression(call) => {
let callee_type = self.infer_type(&call.callee)?;
self.resolve_return_type(&callee_type, &call.arguments)
}
// Promise 类型推断
JsExpression::AwaitExpression(await_expr) => {
let inner = self.infer_type(&await_expr.argument)?;
self.resolve_promise_inner_type(&inner)
}
_ => None,
}
}
}
noFloatingPromises 规则是类型感知 Lint 的明星示例。它能检测出未被 await 或 .catch() 处理的 Promise:
// 会被 noFloatingPromises 标记为错误
async function fetchData() {
fetch('/api/data'); // ❌ Promise 被丢弃!
const result = fetch('/api/data'); // ❌ Promise 被丢弃!
}
// 正确写法
async function fetchData() {
await fetch('/api/data'); // ✅ 已 await
const result = await fetch('/api/data'); // ✅ 已 await
return result;
}
根据 Biome 团队的测试,类型感知规则能检测到 约 75% 的 @typescript-eslint 能检测到的问题,但性能开销只有后者的 1/10。
跨文件分析:模块图的力量
Biome v2 引入了 文件扫描器(File Scanner),能扫描整个项目并构建模块图:
// Biome 的模块图构建逻辑
pub struct ModuleGraph {
modules: HashMap<ModuleId, ModuleInfo>,
edges: Vec<ModuleEdge>,
}
impl ModuleGraph {
// 构建模块图
pub fn build(project_root: &Path) -> Self {
let mut graph = ModuleGraph::new();
// 扫描所有 .ts/.tsx/.js/.jsx 文件
for file in scan_files(project_root) {
let module = parse_module(&file);
graph.add_module(module);
// 分析 import/export 语句
for import in module.imports() {
let resolved = resolve_import(&file, &import);
graph.add_edge(module.id(), resolved);
}
}
graph
}
}
noUnusedClasses 和 noUndeclaredClasses 是跨文件分析的典型应用:
# 检测未使用的 CSS 类
$ biome check --linter-enabled=true src/
src/components/Button.tsx:4:28 lint/nursery/noUndeclaredClasses
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
i The CSS class btn-invalid is not defined in any imported stylesheet.
3 │ export function Button() {
> 4 │ return <button className="btn-invalid">Click</button>
│ ^^^^^^^^^^^
5 │ }
6 │ }
i Checked import tree:
Button.jsx (this file)
└─ imported by:
├─ Block.jsx
│ └─ imported by:
│ └─ App.jsx -> which imports app.css
└─ Page.jsx
└─ imported by:
└─ App.jsx -> which imports app.css
第四层:Monorepo 支持——嵌套配置与包感知
Biome v2 对 Monorepo 的支持是一个被低估的特性。它引入了 嵌套配置文件 机制:
// 根目录 biome.json
{
"root": true,
"linter": {
"rules": {
"recommended": true
}
}
}
// packages/ui/biome.jsonc(嵌套配置)
{
"root": false, // 标记为嵌套配置
"extends": "//", // 继承根配置的微语法
"linter": {
"rules": {
"a11y": {
"useValidAriaRole": "error" // UI 包特有的 a11y 规则
}
}
}
}
// packages/api/biome.jsonc
{
"root": false,
"extends": "//",
"linter": {
"rules": {
"correctness": {
"noFloatingPromises": "error" // API 包特有的 Promise 规则
}
}
}
}
包感知(Package-Aware) 是另一个关键能力。当 Lint 规则需要读取 package.json 时,它会使用正确的 package.json:
// 包感知的依赖检查
pub fn check_restricted_dependencies(
module: &Module,
graph: &ModuleGraph,
) -> Vec<Diagnostic> {
// 找到这个模块所属的 package
let package = graph.find_package_for_module(module);
// 使用正确的 package.json 中的 dependencies
for dep in &module.dependencies() {
if is_restricted_dependency(dep) {
// 检查是否在 package.json 的 dependencies 中
if package.has_dependency(dep) {
return vec![Diagnostic::RestrictedDependency {
name: dep.to_string(),
message: format!(
"Package '{}' is restricted. Consider using '{}' instead.",
dep,
suggest_alternative(dep)
),
}];
}
}
}
vec![]
}
第五层:插件系统——GritQL 的整合
Biome v2.5 引入了 GritQL 插件系统,这是一个基于代码模式匹配的查询语言:
// GritQL 示例:查找所有 console.log 调用
`console.log($msg)` => {
message: "Found console.log: $msg"
}
// GritQL 示例:将 var 转换为 const
`var $x = $init` => `const $x = $init`
// GritQL 示例:查找未使用的导入
import $default from $source where {
$default <: `$_unused`,
message: "Unused default import from $source"
}
GritQL 的整合意味着你可以编写自定义的代码转换规则,而不需要深入 Biome 的 Rust 内部:
# 使用 GritQL 插件
$ biome check --plugin=gritql --plugin-config=gritql.json src/
# gritql.json
{
"patterns": [
{
"name": "no-any-type",
"pattern": "any",
"message": "Avoid using 'any' type",
"fix": "unknown"
}
]
}
第六层:性能工程——为什么 Biome 这么快
多线程并行
Biome 的所有操作都是多线程的。它使用 Rust 的 rayon crate 进行数据并行:
// Biome 的并行处理逻辑
use rayon::prelude::*;
pub fn lint_files(files: Vec<FilePath>) -> Vec<Diagnostic> {
files
.par_iter() // 并行迭代
.flat_map(|file| {
let source = read_file(file);
let cst = parse(&source);
let module = analyze(&cst);
lint_module(&module)
})
.collect()
}
增量缓存
Biome 会缓存每个文件的 CST 和分析结果。当文件没有变化时,直接使用缓存:
// 增量缓存逻辑
pub struct LintCache {
entries: HashMap<FileHash, CacheEntry>,
}
impl LintCache {
pub fn get_or_compute(
&mut self,
file: &Path,
compute: impl FnOnce(&str) -> LintResult,
) -> LintResult {
let content = read_file(file);
let hash = hash_content(&content);
if let Some(entry) = self.entries.get(&hash) {
// 文件没变,使用缓存
return entry.result.clone();
}
// 文件变了,重新计算
let result = compute(&content);
self.entries.insert(hash, CacheEntry {
result: result.clone(),
timestamp: now(),
});
result
}
}
内存效率
Biome 的 CST 设计使得内存使用非常高效。一个 10,000 行的 TypeScript 文件,CST 只占约 500KB 内存,而 TypeScript Compiler 的 AST 需要约 5MB——10 倍的差距。
第七层:从 ESLint + Prettier 迁移到 Biome
迁移路径
# 1. 安装 Biome
npm install --save-dev --save-exact @biomejs/biome
# 2. 初始化配置
npx @biomejs/biome init
# 3. 从 ESLint 迁移规则
npx @biomejs/biome migrate eslint
# 4. 从 Prettier 迁移配置
npx @biomejs/biome migrate prettier
# 5. 运行检查
npx @biomejs/biome check --write ./src
# 6. 卸载旧工具
npm uninstall eslint prettier eslint-config-prettier eslint-plugin-react \
@typescript-eslint/parser @typescript-eslint/eslint-plugin \
eslint-plugin-import eslint-import-resolver-typescript
性能对比:真实项目
我们用一个包含 5,000 个 TypeScript 文件的真实项目进行对比:
| 操作 | ESLint + Prettier | Biome | 提升倍数 |
|---|---|---|---|
| 冷启动 lint | 45.2s | 1.8s | 25x |
| 增量 lint | 8.3s | 0.3s | 28x |
| 格式化 | 32.1s | 0.9s | 36x |
| CI 耗时 | 127s | 4.2s | 30x |
| node_modules 大小 | 187MB | 12MB | 16x |
配置文件对比
ESLint + Prettier(通常需要 2-3 个配置文件):
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"prettier"
],
"rules": {
"no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn"
}
}
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80
}
// .prettierignore
node_modules
dist
build
Biome(一个配置文件):
// biome.json
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"files": {
"ignore": ["node_modules", "dist", "build"]
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedImports": "error"
}
}
}
}
第八层:生态整合——不仅仅是 CLI
IDE 支持
Biome 提供了 VS Code、Neovim、Helix 等编辑器的原生支持:
// VS Code settings.json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
CI/CD 集成
# GitHub Actions 配置
name: Biome
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: biomejs/setup-biome@v2
with:
version: latest
- run: biome ci ./src
与其他工具的互操作
Biome 可以与现有工具链共存:
// biome.json - 与 ESLint 共存的过渡配置
{
"linter": {
"enabled": true,
"rules": {
"recommended": true
// 只启用 Biome 特有的规则
// ESLint 的规则由 ESLint 继续处理
}
},
"formatter": {
"enabled": true // 禁用 Biome 格式化,继续用 Prettier
}
}
2026 Roadmap:Biome 的下一步
根据 Biome 官方的 2026 路线图,未来的重点包括:
- 编译器(Compiler):Biome 正在构建一个完整的 JavaScript/TypeScript 编译器,目标是替代 Babel 和 tsc
- 打包器(Bundler):基于 Rust 的增量打包器
- 测试运行器(Test Runner):集成测试框架
- 更完整的类型推断:逐步缩小与 TypeScript Compiler 的差距
- GritQL 插件生态:建立一个类似 ESLint 插件的社区生态
总结:为什么你应该关注 Biome
Biome 不是又一个"更好的 ESLint"或"更快的 Prettier"。它是一个从第一性原理重新设计的 JavaScript 工具链,核心创新在于:
- CST 而非 AST:保留所有源代码信息,实现增量处理
- 零依赖:不需要 TypeScript 编译器就能做类型感知 Lint
- 跨文件分析:模块图支持,CSS 类检查,包感知
- 统一工具链:格式化、Lint、导入组织,一个工具搞定
- 极致性能:Rust + 多线程 + 增量缓存,比传统工具快 25-36 倍
对于新项目,Biome 已经是 2026 年的最佳选择。对于老项目,可以先以"格式化器"的身份引入 Biome(替代 Prettier),然后逐步迁移 Lint 规则——这是一个渐进式的、低风险的迁移路径。
JavaScript 工具链的十年之痛,正在被 Rust 一行一行地终结。
相关资源:
- Biome 官方文档:https://biomejs.dev
- GitHub 仓库:https://github.com/biomejs/biome
- GritQL 文档:https://docs.grit.io
- 2026 Roadmap:https://biomejs.dev/blog/roadmap-2026/