Превью JS-плагинов Oxlint
Эта заметка анонсирует превью JS-плагинов Oxlint. JS-плагины уже вышли в альфу! См. анонс альфы JS-плагинов Oxlint про актуальные возможности и улучшения.
Ранее в этом году мы спросили сообщество, чтобы определить дизайн поддержки пользовательских JS-плагинов в Oxlint. Сегодня рады показать результат месяцев исследований, прототипирования и разработки:
Oxlint поддерживает плагины на JS!
Ключевые возможности
- ESLint-совместимый API плагинов. Oxlint сможет запускать многие существующие плагины ESLint без изменений.
- Альтернативный API — немного другой и даёт лучшую производительность.
Что это и чем не является
Это превью — только начало. Важно понимать:
- В первом релизе реализована не вся ESLint plugin API.
- Производительность уже хорошая, но станет значительно выше — впереди много оптимизаций.
Наиболее используемые API для правил проверки кода уже есть, поэтому многие правила ESLint уже работают. API для токенов отсутствуют — стилистические (форматирующие) правила пока не поддерживаются.
Попробуйте, пришлите фидбек и помогите расставить приоритеты следующей фазы.
В заметке
- Как пользоваться.
- Что дальше.
- Технические детали «иметь торт и съесть»: ESLint-совместимость и высокая производительность.
Quick Start
Установите Oxlint:
pnpm add -D oxlintНапишите простой JS-плагин:
// 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;Создайте конфиг с плагином:
// .oxlintrc.json
{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/no-debugger": "error"
}
}Добавьте файл для линтинга:
// foo.js
debugger;Запуск:
pnpm oxlintОжидаемый вывод:
x best-plugin-ever(no-debugger): No debugger!
,-[foo.js:1:1]
1 | debugger;
: ^^^^^^^^^
`----Подробнее о написании плагинов — документация.
Alternative API
У Oxlint есть чуть другой API для лучшей производительности.
Этот альтернативный API даёт плагины, совместимые и с ESLint, и с Oxlint.
Пример правила: предупреждать, если в файле больше пяти объявлений классов:
Вариант ESLint
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};Альтернативный API
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 });
}
},
};
},
});Отличия
- Обёртка правила в
defineRule(...).
- const rule = {
+ const rule = defineRule({- Вместо
createиспользуйтеcreateOnce.
- 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 — только один раз.
Остальные API ведут себя как в ESLint.
Зачем альтернативный API может сильно ускорять работу — в документации.
Performance
Как уже сказано, в этом первом превью мы не ставили производительность во главу. Цель — достаточно полный API для реальных проектов и фидбек ранних пользователей.
Сейчас скорость приличная, но не предел.
Важнее другое: прототип следующей версии показывает, что выбранная архитектура способна на исключительную производительность после серии оптимизаций (см. Under the hood).
В ближайшие месяцы будем их включать — ожидайте кратное ускорение относительно текущей версии.
Даже без этих оптимизаций Oxlint остаётся конкурентоспособным.
Oxlint против ESLint на среднем TS-проекте vuejs/core:
| Linter | Time |
|---|---|
| ESLint | 4,116 ms |
| ESLint multi-threaded | 3,710 ms |
| Oxlint | 48 ms |
| Oxlint with custom JS plugin | 236 ms |
Details
INFO
- Benchmark repo: https://github.com/overlookmotel/vue-core-cam/tree/bench-js-plugins
- Benchmarked on MacBook Air M3, 24GB RAM
- Bench command:
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'Примечание: версия Oxlint в NPM на момент написания (1.23.0) содержала баг, из‑за которого этот бенчмарк сильно занижает стоимость JS-плагинов. Приведённые цифры получены на последнем main после исправления, коммит cd266b4c101c35c33e122457cdd0b514b44597a9. См. также ниже.
В этом примере простой JS-плагин заметно стоит дороже, но Oxlint всё равно примерно в 15 раз быстрее ESLint, даже с новым многопоточным раннером ESLint.
Очевидно, сложные или множественные JS-плагины увеличат затраты.
Features
Oxlint поддерживает большую часть ESLint API, обычно нужной правилам только на обходе AST. В том числе большинство правил с исправлениями кода.
Токенные API пока не поддерживаются — стилистические правила не работают.
Supported
- AST traversal
- AST exploration (
node.parent,context.sourceCode.getAncestors) - Fixes
- Selectors (ESLint docs)
SourceCodeAPIs (e.g.context.sourceCode.getText(node))
Not supported yet
- Language server (IDE) support
- Rule options
- Suggestions
Scope analysis(implemented since v1.25.0)SourceCodeAPIs related to tokens and comments (e.g.context.sourceCode.getTokens(node))- Control flow analysis
What's next
В ближайшие месяцы:
1. Расширение поверхности API плагинов
Цель — 100% ESLint plugin API, чтобы Oxlint мог запускать любой плагин ESLint без правок.
2. Производительность
Уже неплохо, но прототипы показали большой запас от дальнейших оптимизаций. Будем применять их и приближать JS-плагины к скорости Rust.
Under the hood
Дальше — необязательные «гиковские» детали реализации.
Главный вопрос: совместимость с ESLint или нет?
Мы задавали сообществу: нужен ли Oxlint ESLint-совместимый API плагинов.
Совместимость удобна для знакомства и миграции с ESLint.
Но Oxlint известен производительностью — терять её не хочется.
Задача прототипов — измерить компромисс между скоростью и совместимостью и найти решение «и торт, и поесть»: ESLint-совместимый API и приемлемая скорость («приемлемая» здесь — очень быстрая).
Считаем, что комбинацией подходов этого можно добиться.
Alternative API
Почему альтернативный API даёт потенциал к большей скорости — в документации.
Raw transfer
Инструменты вроде Oxc представляют код JS/TS как AST (abstract syntax tree). AST очень объёмные — намного больше исходного текста.
Обычно главный тормоз быстрого взаимодействия JS и нативного кода вроде Rust — сериализация и десериализация таких структур между «мирами».
Простейший путь передать AST из Rust в JS — сериализовать в JSON, передать строкой и «оживить» через JSON.parse. Это очень медленно; часто дороже, чем выигрыш от нативного кода. Другие форматы эффективнее JSON, но накладные расходы всё равно велики.
Мы сделали схему «raw transfer» без сериализации: используется нативный layout памяти Rust как формат данных (подробности).
«Raw transfer» — основа текущей реализации JS-плагинов.
Lazy deserialization
Вторая большая проблема производительности, особенно при JS в воркерах на нескольких ядрах, — сборщик мусора. Каждый объект нужно потом уничтожить; в JS это делает GC. Движки вроде V8 оптимизированы, но GC дорог и отнимает CPU у полезной работы.
У нас есть прототип AST-visitor с ленивой десериализацией — создаются только те части дерева, которые действительно нужны.
Например, для правила про классы visitor быстро проходит большую часть AST и создаёт JS-объекты только для узлов ClassDeclaration. Для остального (переменные, if, функции…) узлы можно не материализовать.
Эффект двойной:
- Raw transfer убирает стоимость сериализации; лень резко снижает и десериализацию.
- Сильно меньше давление на GC.
Похожий путь у Deno — отлично описан в посте Marvin Hagemeister; линтер Deno очень эффективен.
Мы видим, что комбинация ленивой десериализации и raw transfer даёт действительно высокую скорость: без этих двух накладных расходов JS-плагины работают намного быстрее.
Эта оптимизация ещё не в текущей версии JS-плагинов; появится в будущем релизе.
Try it out!
Попробуйте JS-плагины и поделитесь опытом — любой фидбек полезен.
Если не хватает API для ваших плагинов — напишите: будем закрывать пробелы в ближайшие месяца с приоритетом по спросу.
Удачного линтинга!
Edit: 18th Oct 2025
В первоначальной версии заметки от 9 октября были приведены результаты бенчмарка, сильно завышавшие производительность JS-плагинов Oxlint. Причиной был баг в Oxlint: при определённых конфигурациях с overrides JS-плагины пропускались на многих файлах. Из‑за этого мы переоценили скорость JS-плагинов в цитируемых бенчмарках.
Приносим искренние извинения и благодарим Herrington Darkholme за указание на ошибку.

