今年早些时候,我们向社区征求了意见,以为 Oxlint 支持自定义 JS 插件的设计提供参考。今天,我们很高兴地宣布经过数月的研究、原型开发以及最终构建后的成果:
🌐 Earlier this year we asked for input from the community to inform design for Oxlint support for custom JS plugins. Today, we are pleased to announce the result of many months of research, prototyping, and finally building:
Oxlint 支持用 JS 编写的插件!
主要特点
🌐 Key features
- 与 ESLint 兼容的插件 API。Oxlint 将能够在无需修改的情况下运行许多现有的 ESLint 插件。
- 一个略有不同的替代 API,并且可以释放更好的性能。
这是什么以及不是什么
🌐 What this is and isn't
此预览版本仅仅是个开始。需要注意的是:
🌐 This preview release is just the beginning. It is important to note that:
- 此初始版本未实现 ESLint 的所有插件 API。
- 性能很好,但它会变得 非常 更好——我们有许多优化正在进行中。
用于代码检查规则的最常用 API 已经实现,因此许多现有的 ESLint 规则已经可以使用。但与 token 相关的 API 缺失,所以样式(格式)规则将无法使用。
🌐 The most commonly-used APIs for code-checking rules are implemented, so many existing ESLint rules will already work. But token-related APIs are absent, so stylistic (formatting) rules will not.
我们邀请用户试用、提供反馈,并帮助我们确定下一阶段开发的优先事项。
🌐 We invite users to take it for a spin, give feedback, and inform our priorities for the next phase of development.
这篇博客文章涵盖
🌐 This blog post covers
- 如何使用它。
- 接下来会发生什么。
- 一些技术细节使我们能够采用“鱼与熊掌兼得”的方法,同时提供 ESLint 兼容性 和 出色的性能。
快速开始
🌐 Quick Start
在你的项目中安装 Oxlint:
🌐 Install Oxlint in your project:
pnpm add -D oxlint编写自定义 JS 插件:
🌐 Write a custom JS plugin:
// plugin.js
// The simplest rule of all - no debugger
const rule = {
create(context) {
return {
DebuggerStatement(node) {
context.report({
message: "No debugger!",
node,
});
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"no-debugger": rule,
},
};
export default plugin;创建启用插件的配置文件:
🌐 Create config file enabling the plugin:
// .oxlintrc.json
{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/no-debugger": "error"
}
}添加要进行代码检查的文件:
🌐 Add a file to be linted:
// foo.js
debugger;运行 Oxlint:
🌐 Run Oxlint:
pnpm oxlint预计将看到:
🌐 Expect to see:
x best-plugin-ever(no-debugger): No debugger!
,-[foo.js:1:1]
1 | debugger;
: ^^^^^^^^^
`----有关插件开发的更多详情,请参阅文档。
🌐 For further details on authoring plugins, see the docs.
替代 API
🌐 Alternative API
Oxlint 还提供了一个略有不同的 API,可以实现更好的性能。
🌐 Oxlint also offers a slightly different API which unlocks better performance.
这个替代 API 生成的插件兼容 ESLint 以及 Oxlint。
示例规则,用于标记包含超过 5 个类声明的文件:
🌐 Example rule that flags files containing more than 5 class declarations:
ESLint 版本
🌐 ESLint version
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};备用 API 版本
🌐 Alternative API version
import { defineRule } from "oxlint";
const rule = defineRule({
createOnce(context) {
// Define counter variable
let classCount;
return {
before() {
// Reset counter before traversing AST of each file
classCount = 0;
},
// Same as before
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
});差异
🌐 The differences
- 将规则对象封装在
defineRule(...)中。
- const rule = {
+ const rule = defineRule({- 使用
createOnce替代create。
- create(context) {
+ createOnce(context) {- 将
create正文中的任何每文件设置移动到before钩子中。
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // Reset counter
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
});这是唯一显著的区别——create(ESLint的方法)会针对每个文件重复调用,而createOnce只调用一次。
🌐 This is the only significant difference - create (ESLint's method) is called repeatedly for each file, whereas createOnce is called once only.
所有其他 API 的行为与 ESLint 中完全相同。
🌐 All other APIs behave exactly the same as in ESLint.
这一替代 API 具有极大提升性能潜力的原因在文档中有说明。
🌐 The reasons why this alternative API has potential to greatly improve performance are explained in the docs.
性能
🌐 Performance
如上所述,在 Oxlint JS 插件的这一初始预览版本中,性能并不是我们的重点。我们的主要目标是完善足够的 API,使 JS 插件能够在实际项目中使用,并收集早期采用者的反馈。
🌐 As mentioned above, performance has not been our focus in this initial preview release of Oxlint JS plugins. Our primary goal has been to fill out enough of the API for JS plugins to be useful in real world projects, and gather feedback from early adopters.
目前表现还可以,但绝不是非常出色。
🌐 Performance at present is decent, but not by any means stellar.
然而——我们认为这是关键点——我们下一个版本的原型展示了,我们已经确定的架构设计在加入各种优化后,能够实现卓越的性能(见技术细节)。
🌐 However - and we feel this is the important point - our prototype of the next version demonstrates that the architectural design we've settled on is capable of exceptional performance, once various optimizations are added into the mix (see Under the hood).
我们将在接下来的几个月里逐步应用这些优化,用户将会看到相较于当前版本有多倍的速度提升。
🌐 We'll be applying those optimization over the course of the next few months, and users will see multiple x speed-ups compared to the current version.
话虽如此,即使没有那些优化,Oxlint 的性能仍然具有竞争力。
🌐 That said, even without those optimizations, Oxlint's performance is still competitive.
Oxlint 与 ESLint 对中型 TypeScript 项目 vuejs/core 的代码检查比较:
🌐 Oxlint vs ESLint linting a medium-sized TypeScript project vuejs/core:
| Linter | 时间 |
|---|---|
| ESLint | 4,116 毫秒 |
| ESLint 多线程 | 3,710 毫秒 |
| Oxlint | 48 毫秒 |
| Oxlint 带自定义 JS 插件 | 236 毫秒 |
详情
INFO
- 基准库: https://github.com/overlookmotel/vue-core-cam/tree/bench-js-plugins
- 在配备 24GB 内存的 MacBook Air M3 上进行基准测试
- 长凳指令:
hyperfine -i --warmup 3 \
'./node_modules/.bin/oxlint --silent' \
'./node_modules/.bin/oxlint -c .oxlintrc-with-custom-plugin.json --silent' \
'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint .' \
'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint . --concurrency=auto'注意:撰写本文时 NPM 上的 Oxlint 版本(1.23.0)存在一个影响此基准测试的错误,并且严重低估了 JS 插件的成本。以上结果是使用最新的 main 分支在修复该错误后获得的,参见 此提交。 请 also 参见 下方。
🌐 Note: The version of Oxlint on NPM at time of writing (1.23.0) has a bug which affects this benchmark, and hugely underestimates the cost of JS plugins. The above results were obtained using latest main branch, after the bug fix, at this commit. Please also see below.
在这个例子中,向 Oxlint 添加一个简单的 JS 插件确实有显著的成本,但即便使用 ESLint 的新多线程运行器,Oxlint 仍然比 ESLint 快 15 倍。
🌐 In this example, adding a simple JS plugin to Oxlint does have a significant cost, but Oxlint is still 15x faster than ESLint, even using ESLint's new multi-threaded runner.
显然,更复杂的 JS 插件,或者许多插件,会带来更高的性能成本。
🌐 Obviously, more complicated JS plugins, or many of them, will have a higher performance cost.
特性
🌐 Features
Oxlint 支持大多数 ESLint 的 API,这些 API 通常用于仅依赖 AST 检查的插件/规则中。 这包括大多数“修复代码”类型的规则。
🌐 Oxlint supports most of ESLint's APIs which are typically used in plugins/rules which rely only on AST inspection. That includes most "fix code"-type rules.
它尚不支持基于令牌的 API,因此风格(格式)规则暂时无法使用。
🌐 It does not yet support token-based APIs, so stylistic (formatting) rules will not work yet.
支持
🌐 Supported
- AST 遍历
- AST 探索(
node.parent、context.sourceCode.getAncestors) - 修复
- 选择器(ESLint 文档)
SourceCodeAPI(例如context.sourceCode.getText(node))
尚不支持
🌐 Not supported yet
- 语言服务器(IDE)支持
- 规则选项
- 建议
范围分析(自 v1.25.0 起已实现)SourceCode与令牌和注释相关的 API(例如context.sourceCode.getTokens(node))- 控制流分析
接下来是什么
🌐 What's next
在接下来的几个月里,我们将会:
🌐 Over the next few months, we will be:
1. 填写插件 API 接口
🌐 1. Filling out the plugin API surface
目标是支持 ESLint 插件 API 的 100% 功能,这样 Oxlint 最终将能够无需修改就运行任何 ESLint 插件。
🌐 Aim is to support 100% of ESLint's plugin API surface, so that Oxlint will eventually be able to run any ESLint plugin without modification.
2. 提高性能
🌐 2. Improving performance
性能已经相当不错,但在原型开发过程中我们已经证明,通过进一步优化可以获得许多显著的性能提升。我们将应用这些优化,使 Oxlint 中的 JS 插件运行速度尽可能接近 Rust。
🌐 Performance is already decent, but we have proven during our prototyping many significant performance gains from further optimizations. We will apply them, and make JS plugins in Oxlint run at as close to Rust speed as we can get.
引擎盖下
🌐 Under the hood
本文剩余部分并不是使用 Oxlint 进行 JS 插件所必需的。但如果你对我们实现方式的技术细节感兴趣,请继续阅读……
🌐 The rest of this post is not necessary to use JS plugins with Oxlint. But if you're interested in the geeky details of how our implementation works, read on...
大问题:要不要与 ESLint 兼容?
🌐 The big question: To ESLint compat or not to ESLint compat?
我们今年早些时候向社区提出的问题是,Oxlint 是否应该目标为与 ESLint 兼容的插件 API。
🌐 The question which we posed to the community earlier this year was whether Oxlint should aim for an ESLint-compatible plugin API or not.
显而易见,就熟悉度和从 ESLint 迁移的便利性而言,兼容 ESLint 的接口是理想选择。
🌐 Obviously, an ESLint-compatible interface is ideal in terms of familiarity and ease of migration from ESLint.
然而,Oxlint 以其出色的性能而闻名,过多地妥协这一点是不可取的。
🌐 However, Oxlint is known for its excellent performance, and compromising that too much would not be desirable.
我们过去几个月原型工作的主要目标是量化性能与 ESLint 兼容性之间的权衡,并探讨是否存在一种“既能吃蛋糕又能保留”的解决方案,同时满足两者——提供 ESLint 兼容的 API 并且性能可接受(这里的“可接受”意味着非常快!)
🌐 The main aim of our prototyping work over past few months has been to quantify what is the trade-off between performance and ESLint compatibility, and to investigate if there's a "have cake and eat it" solution which satisfies both - providing an ESLint-compatible API and acceptable performance ("acceptable" here means pretty damn fast!)
我们相信,通过结合不同的方法,我们已经找到了一种满足两方面需求的方式。
🌐 We believe that through a combination of different approaches, we've found a way to satisfy both demands.
替代 API
🌐 Alternative API
请参阅文档中的说明 in docs,了解此 API 如何释放更高性能的潜力。
🌐 See explanation in docs of why this API unlocks potential for higher performance.
原始传输
🌐 Raw transfer
像 Oxc 这样的工具将 JS/TS 文件的代码表示为“AST”(抽象语法树)。 AST 通常非常大——远远比它们所表示的源代码大得多。
🌐 Tools like Oxc represent the code of a JS/TS file as an "AST" (abstract syntax tree). ASTs are really big - much much larger than the source code they represent.
通常,JS 与像 Rust 这样的本地语言之间实现高性能互操作性的最大障碍是序列化和反序列化,因为在“两个世界”之间传输如此大的数据结构时需要这些操作。
🌐 Typically, the biggest barrier to performant interop between JS and native languages like Rust is the serialization and deserialization involved in transferring such large data structures between the "two worlds".
在 JS 和 Rust 之间移动 AST 最简单、最常见的方法是:将 AST 序列化为 JSON,以字符串形式发送到 JS,然后用 JSON.parse 再次“复原”。但是,这种方法非常慢。通常,这些转换的开销非常高,以至于远远超过了最初使用本地代码带来的性能提升。其他序列化格式比 JSON 更高效,但它们仍然有相当大的开销。
🌐 The simplest and most common way to move an AST between JS and Rust is: Serialize the AST to JSON, send it across to JS as a string and then "rehydrate" it again with JSON.parse. But this is extremely slow. Often the cost of these conversions is so high that they massively outweigh the performance gain of using native code in the first place. Other serialization formats are more efficient than JSON, but they still have a sizeable overhead.
我们开发了一种“原始传输”方案,它完全省去了序列化的过程,通过使用 Rust 的原生内存布局作为序列化格式(有关其工作原理的更多详细信息,请参见 这里)。
🌐 We have developed a scheme "raw transfer" which cuts out serialization altogether, by using Rust's native memory layout as the serialization format (more details on how it works here).
“原始传输”是当前 JS 插件实现的基础。
延迟反序列化
🌐 Lazy deserialization
良好性能的第二大敌人,尤其是在通过 worker 线程在多个 CPU 核心上运行 JS 时,是垃圾收集器。每个你创建的对象也需要被销毁以回收它的内存。在 JS 中,这项工作由垃圾收集器完成。像 V8 这样的 JS 引擎经过高度优化,但垃圾收集仍然是一个昂贵的过程,并且 GC 会从实际工作负载中“窃取” CPU 资源。
🌐 The 2nd biggest enemy of good perf, particularly when running JS across multiple CPU cores in worker threads, is the garbage collector. Every object you create also needs to be destroyed to recover its memory. In JS, this is the job of the garbage collector. JS engines like V8 are highly optimized, but still garbage collection is an expensive process, and GC "steals" CPU resources from the actual workload.
我们已经原型化了一个 AST 访问器,它可以对 AST 进行“懒惰”反序列化,并且只反序列化那些实际上“需要”反序列化的 AST 部分。
🌐 We have prototyped an AST visitor which deserializes the AST lazily, and only deserializes the parts of the AST which actually need to be.
例如,如果你的 lint 规则与类声明相关,这个访问器将会快速遍历大部分 AST,而不做太多处理,只会为 ClassDeclaration AST 节点创建 JS 对象,然后将它们传递给规则的代码进行处理。对于 AST 的其他部分(变量声明、if 语句、函数等),根本不需要创建节点对象。
🌐 For example, if your lint rule relates to class declarations, this visitor will fly through most of the AST without doing much, and will only create JS objects for ClassDeclaration AST nodes, which are then passed to the rule's code to process. For the rest of the AST (variable declarations, if statements, functions, etc) there is no need to create node objects at all.
这有两个效果:
🌐 This has 2 effects:
- 原始传输将序列化的成本降为零。惰性评估也大大降低了另一端(反序列化)的开销。
- 大大减少了垃圾收集器的压力。
Deno 采取了类似的方法,这在 Marvin Hagemeister 的博客文章 中解释得非常清楚,而且 Deno lint 的实现非常高效。
🌐 Deno has taken a similar approach, which is explained brilliantly in Marvin Hagemeister's blog post, and Deno lint has a superbly efficient implementation.
然而,我们发现,正是懒惰反序列化与“原始传输”的结合,才真正带来了优秀的性能。我们的测试发现,去掉这两个开销后,JS 插件的运行速度可以快得多。
🌐 However, we've found that it's the combination of lazy deserialization with "raw transfer" which delivers really good performance. Our tests have found that, with both these overheads removed, JS plugins can run at much greater speed.
此优化尚未包含在当前版本的 JS 插件中。我们将在未来的版本中实现它。
🌐 This optimization is not yet included in current version of JS plugins. We will implement it in a future version.
试试看!
🌐 Try it out!
请尝试使用 JS 插件并反馈你的体验。无论是正面还是负面的意见,我们都非常感激。
🌐 Please try out JS plugins and report your experience. All feedback - either positive or negative - is gratefully received.
特别是,如果你发现 Oxlint 缺少你插件所需的一些 API,请告诉我们。我们将在接下来的几个月内补充这些 API,并会优先考虑需求量最大的部分。
🌐 In particular, if you find that Oxlint is lacking some of the APIs you need for your plugins to work, please let us know. We'll be filling in the gaps in the API over the next few months, and will prioritise those for which there's greatest demand.
祝你代码检查愉快!
🌐 Happy linting!
编辑:2025年10月18日
🌐 Edit: 18th Oct 2025
这篇博客文章的原始版本于 10 月 9 日发布,其中包含的基准测试结果显示 Oxlint JS 插件的性能远远优于实际情况。这是由于 Oxlint 中的一个错误导致的,当配置包含覆盖时,该错误会在某些情况下跳过许多文件的 JS 插件。这个错误导致我们引用的基准测试中 JS 插件的性能被严重高估。
🌐 The original version of this blog post published on 9th Oct contained benchmarks results which showed the performance of Oxlint JS plugins to be far better than they are in reality. This was the result of a bug in Oxlint which was causing JS plugins to be skipped on many files in certain circumstances when the config contains overrides. This bug lead to the performance of JS plugins being way overestimated in the benchmarks we quoted.
我们对这个错误表示诚挚的歉意,并感谢 Herrington Darkholme 指出这个错误。
🌐 We sincerely apologise for this mistake, and thank Herrington Darkholme for pointing out the error.
