Skip to content

Oxlint JS 插件预览


今年早些时候,我们曾向社区征集意见 以指导 Oxlint 自定义 JS 插件的设计。如今,我们很高兴地宣布经过数月的研究、原型开发,最终实现的结果:

Oxlint 支持以 JS 编写的插件!

主要特性

  • 与 ESLint 兼容的插件 API。Oxlint 可以无需修改直接运行许多现有的 ESLint 插件。
  • 一种略有不同的替代 API,能够实现更优的性能。

这是什么,以及这不是什么

此预览版仅是起点。需要注意的是:

  • 此次发布并未实现 ESLint 插件 API 的全部功能。
  • 性能表现良好,但未来将会有大幅提升——我们已有多个优化方案在规划中。

目前用于代码检查规则的最常用 API 已实现,因此许多现有的 ESLint 规则可直接使用。但与标记相关的 API 尚未支持,因此样式(格式化)类规则尚不可用。

我们诚邀用户试用并提供反馈,帮助我们确定下一阶段开发的优先级。

本文涵盖内容

  1. 如何使用。
  2. 即将推出的新功能。
  3. 一些技术细节,解释了我们如何实现“既拥有蛋糕,又能吃掉它”的策略——同时提供 ESLint 兼容性 优异性能。

快速入门

在项目中安装 Oxlint:

sh
pnpm add -D oxlint

编写一个自定义的 JS 插件:

js
// plugin.js

// 最简单的规则:禁止使用 debugger
const rule = {
  create(context) {
    return {
      DebuggerStatement(node) {
        context.report({
          message: "禁止使用 debugger!",
          node,
        });
      },
    };
  },
};

const plugin = {
  meta: {
    name: "best-plugin-ever",
  },
  rules: {
    "no-debugger": rule,
  },
};

export default plugin;

创建配置文件以启用该插件:

json
// .oxlintrc.json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/no-debugger": "error"
  }
}

添加一个待检查的文件:

js
// foo.js
debugger;

运行 Oxlint:

sh
pnpm oxlint

预期输出如下:

 x best-plugin-ever(no-debugger): 禁止使用 debugger!
  ,-[foo.js:1:1]
1 | debugger;
  : ^^^^^^^^^
  `----

有关插件编写更详细的说明,请参见 文档

替代 API

Oxlint 还提供了一种略有不同的 API,可实现更好的性能。

该替代 API 生成的插件不仅兼容 ESLint,也兼容 Oxlint。

示例规则:标记包含超过 5 个类声明的文件。

ESLint 版本

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "类太多", node });
        }
      },
    };
  },
};

替代 API 版本

js
import { defineRule } from "oxlint";

const rule = defineRule({
  createOnce(context) {
    // 定义计数器变量
    let classCount;

    return {
      before() {
        // 在遍历每个文件的 AST 前重置计数器
        classCount = 0;
      },
      // 与之前相同
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "类太多", node });
        }
      },
    };
  },
});

差异对比

  1. 使用 defineRule(...) 包装规则对象。
diff
- const rule = {
+ const rule = defineRule({
  1. 使用 createOnce 而非 create
diff
-   create(context) {
+   createOnce(context) {
  1. 将原本在 create 函数体内的每文件初始化逻辑移至 before 钩子中。
diff
-     let classCount = 0;
+     let classCount;

      return {
+       before() {
+         classCount = 0; // 重置计数器
+       },
        ClassDeclaration(node) {
          classCount++;
          if (classCount === 6) {
            context.report({ message: "类太多", node });
          }
        },
      };
    },
  });

这是唯一的显著差异:create(ESLint 的方法)会为每个文件反复调用,而 createOnce 仅调用一次。

其余所有 API 的行为与 ESLint 完全一致。

关于为何该替代 API 有潜力极大提升性能的原因,详见 文档

性能

如前所述,在此次 Oxlint JS 插件的初始预览版本中,性能并非我们的首要关注点。我们的首要目标是完善足够的 API,使 JS 插件在真实项目中具有实用性,并收集早期使用者的反馈。

当前性能表现尚可,但远谈不上出色。

然而——我们认为这一点至关重要——我们对下一代版本的原型演示表明,我们所采用的架构设计具备实现卓越性能的潜力,一旦加入各项优化(详见 内部原理),性能将大幅提升。

在未来几个月内,我们将逐步应用这些优化,用户将看到相比当前版本快数倍甚至数十倍的性能提升。

尽管如此,即使没有这些优化,Oxlint 的性能仍具竞争力。

使用 Oxlint 对一个中等规模的 TypeScript 项目 vuejs/core 进行检查:

检查工具时间
ESLint4,116 ms
ESLint 多线程模式3,710 ms
Oxlint48 ms
Oxlint + 自定义 JS 插件236 ms
详细信息

INFO

sh
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 分支上获得,提交记录为 此提交。请参见 下方

在此示例中,为 Oxlint 添加一个简单的 JS 插件确实带来显著开销,但即便如此,其速度仍比 ESLint 快 15 倍,甚至优于 ESLint 新的多线程运行器。

显然,更复杂的 JS 插件,或大量插件,将带来更高的性能代价。

功能支持

Oxlint 支持大多数在仅依赖 AST 检查的插件/规则中常用的 ESLint API。这包括大多数“修复代码”类型的规则。

目前尚未支持基于标记(token)的 API,因此样式(格式化)类规则尚不可用。

已支持

  • AST 遍历
  • AST 探索(node.parent, context.sourceCode.getAncestors
  • 修复功能
  • 选择器(ESLint 文档
  • SourceCode API(例如 context.sourceCode.getText(node)

尚未支持

  • 语言服务器(IDE)支持
  • 规则选项
  • 建议功能
  • 作用域分析 (已实现,自 v1.25.0 起)
  • 与标记和注释相关的 SourceCode API(例如 context.sourceCode.getTokens(node)
  • 控制流分析

下一步计划

在未来几个月内,我们将:

1. 完善插件 API 表面

目标是支持 100% 的 ESLint 插件 API 表面,使得 Oxlint 最终能够无需修改即可运行任意 ESLint 插件。

2. 提升性能

性能已属良好,但我们通过原型验证,进一步优化可带来显著的性能提升。我们将逐步实施这些优化,让 Oxlint 中的 JS 插件尽可能接近原生 Rust 的运行速度。

内部原理

本文其余部分并非使用 Oxlint JS 插件所必需。但如果您对实现机制的底层细节感兴趣,请继续阅读……

核心问题:是否追求 ESLint 兼容?

今年早些时候,我们曾向社区提出问题 是否应让 Oxlint 追求 ESLint 兼容的插件 API

显然,从熟悉度和从 ESLint 迁移的便利性角度看,一个 ESLint 兼容的接口是最理想的。

然而,Oxlint 以其出色的性能著称,过度牺牲性能将不可接受。

过去数月原型工作的主要目标,是量化性能与 ESLint 兼容性之间的权衡,并探索是否存在“两全其美”的解决方案——既能提供 ESLint 兼容的 API,又具备可接受的性能(“可接受”此处意为极快!)。

我们相信,通过多种方法的结合,已找到满足双重需求的途径。

替代 API

详见 文档 中的解释,说明为何该 API 能释放更高的性能潜力。

原生传输

像 Oxc 这样的工具将一个 JS/TS 文件的代码表示为“AST”(抽象语法树)。AST 实际上非常庞大——远超其所代表的源代码体积。

通常,影响跨语言(如 JS 与 Rust)高效互操作的最大障碍,是传递此类大型数据结构时涉及的序列化与反序列化开销。

在 JS 与 Rust 之间传递 AST 的最简单常见方式是:将 AST 序列化为 JSON,以字符串形式传入 JS,再通过 JSON.parse 重新“重建”。但这极其缓慢。这类转换的成本往往高得惊人,以至于完全抵消了使用原生代码带来的性能优势。虽然其他序列化格式比 JSON 更高效,但仍存在显著开销。

我们开发了一种名为“原生传输”的方案,通过直接利用 Rust 的原生内存布局作为序列化格式,彻底跳过序列化环节(更多实现细节见 此处)。

“原生传输”是当前 JS 插件实现的基础。

懒加载反序列化

在多核 CPU 的工作线程中运行 JS 时,性能的主要敌人之一是垃圾回收器。你创建的每个对象最终都需要被销毁以释放内存。在 JS 中,这一任务由垃圾回收器完成。尽管 V8 等 JS 引擎高度优化,但垃圾回收本身仍是昂贵的操作,且会“窃取”实际工作负载的 CPU 资源。

我们已原型实现了一种懒加载的 AST 访问器,仅在真正需要时才反序列化部分 AST。

例如,若你的检查规则仅涉及类声明,该访问器将快速遍历大部分 AST,几乎不进行任何操作,仅针对 ClassDeclaration AST 节点创建对应的 JS 对象,然后将其传递给规则代码处理。对于其他部分的 AST(如变量声明、if 语句、函数等),根本不需要创建节点对象。

这带来了两个效果:

  1. 原生传输将序列化成本降至零;懒加载极大地减少了另一侧(反序列化)的开销。
  2. 显著降低垃圾回收器的压力。

Deno 采用了类似方法,由 Marvin Hagemeister 在其博客中精彩阐述 《加速 JavaScript 生态系统(第 11 部分)》,而 Deno lint 实现了极为高效的引擎。

但我们发现,正是“懒加载反序列化”与“原生传输”的结合,带来了真正的卓越性能。我们的测试表明,当这两项开销都被消除后,JS 插件的运行速度大幅提升。

此优化尚未包含在当前版本的 JS 插件中,我们将在未来版本中实现。

试用一下!

欢迎尝试使用 JS 插件,并分享您的体验。无论正面或负面的反馈,我们都衷心感谢。

特别提醒:如果您发现 Oxlint 缺少某些您插件所需的 API 无法使用,请务必告知我们。未来几个月我们将持续填补这些缺口,并优先解决需求最迫切的功能。

祝您编码愉快!


更新:2025 年 10 月 18 日

本博客最初于 10 月 9 日发布,其中包含的基准测试结果表明 Oxlint JS 插件的性能远高于实际情况。这是由于 Oxlint 存在一个缺陷,导致在某些配置包含覆盖项的情况下,部分文件上的 JS 插件被跳过。该缺陷导致我们引用的基准测试结果严重高估了 JS 插件的性能。

我们为此错误深表歉意,并感谢 Herrington Darkholme 指出这一问题。