Пишем плагин к OXC на Rust
• build, rust
Введение
Продолжаем тему прошлых постов про Babel 8 и плагин к SWC, сегодня соберём плагин для трансформации кода через OXC. Будет много отсылок на второй пост.
OXC – ещё один инструмент на Rust, как и SWC. Точнее, набор инструментов. В состав входят парсер, генератор кода из AST, линтер, инструмент форматирования кода, минификации и некоторые другие.
Представим, что нам нужно сделать с исходным кодом что-то произвольное, и попробуем это преобразование сделать с помощью OXC.
Ситуация с плагинами
Если в случае SWC была документация, а также рабочий способ публикации и интеграции плагинов в готовый инструмент, то с OXC-трансформациями ничего этого нет. Можно писать плагины на JS к линтеру (другому инструменту), но конкретно к трансформации пока нельзя. Разработчики опасаются за скорость из-за передачи данных между JS <-> Rust, так как в случае трансформации и наслоения плагинов друг на друга ситуация станет сильно сложнее, чем в линтере.
Чем же мы тогда займёмся? Конечно же, пойдём в репозиторий OXC и будем собирать его из исходников :)
Без шуток, если нам нужен кастомный плагин к OXC, то в репозитории уже есть пример реализации плагинов. Остаётся написать свой плагин, скомпилировать и запустить.
Ну а в конце подведём итоги, измерим скорость и сравним инструменты по удобству.
Пилим плагин
Все правки в виде коммита я положил на ГитХаб, при желании можно обращаться туда. Правок на 60 строк, что намекает, что начать не очень сложно.
Как и в случае с SWC, ставим Rust по официальной инструкции. Таргет для сборки добавлять не требуется, ставить пакеты в cargo – тоже (сами поставятся необходимые, когда потребуется).
В репозитории есть один интересный файл: define.rs.
Клонируем репозиторий, создаём файл test.js в корне со следующим содержимым:
// test.js
console.log(process.env.NODE_ENV);Запускаем:
cargo run -p oxc_transformer_plugins --example define -- test.jsДолжно вывестись console.log(development) (без кавычек, да). Или будет какая-то ошибка. Если вывелось – значит, всё хорошо, сборка работает.
Идея плагина такая же, как и в случае с SWC: будем удалять второй аргумент в функции run. Создаём новый файл:
// crates/oxc_transformer_plugins/src/remove_second_arg.rs
// Импорты
use oxc_ast::ast::*;
use oxc_ast_visit::{VisitMut, walk_mut};
use oxc_semantic::Scoping;
/// Возвращаемое значение для [`RemoveSecondArg::build`].
#[must_use]
pub struct RemoveSecondArgReturn {
pub scoping: Scoping,
pub changed: bool,
}
// Структура для обхода AST
pub struct RemoveSecondArg {
changed: bool,
scoping: Option<Scoping>,
}
impl<'a> VisitMut<'a> for RemoveSecondArg {
// Обработчик обхода вызова функции в AST
fn visit_call_expression(&mut self, call_expr: &mut CallExpression<'a>) {
if let Expression::Identifier(ident) = &call_expr.callee {
if ident.name.as_str() == "run" && call_expr.arguments.len() > 1 {
call_expr.arguments.remove(1);
self.changed = true;
}
}
walk_mut::walk_call_expression(self, call_expr);
}
}
impl RemoveSecondArg {
pub fn new() -> Self {
Self { changed: false, scoping: None }
}
// Основная функция
pub fn build(&mut self, scoping: Scoping, program: &mut Program<'_>) -> RemoveSecondArgReturn {
self.scoping = Some(scoping);
self.visit_program(program);
let scoping = self.scoping.take().unwrap();
RemoveSecondArgReturn { scoping, changed: self.changed }
}
}В файле crates/oxc_transformer_plugins/src/lib.rs дописываем:
// ...
mod remove_second_arg;
// ...
// ...
pub use remove_second_arg::*;
// ...В уже знакомом нам define.rs правим импорт (или просто добавляем)
use oxc_transformer_plugins::RemoveSecondArg;и правим вызов старого плагина с дефайнами на новый:
let _ = RemoveSecondArg::new().build(scoping, &mut program);Правим test.js и добавляем туда run(a, b), запускаем также, как и раньше:
cargo run -p oxc_transformer_plugins --example define -- test.jsЕсли вывелось run(a), то всё получилось.
Собираем релизную версию
Для полного контекста: я прописал следующий конфиг для релизной сборки в crates/oxc_transformer_plugins/Cargo.toml:
[profile.release]
codegen-units = 1
lto = true
opt-level = 3
strip = "symbols"Запускаем сборку:
cargo build --release -p oxc_transformer_plugins --example defineПосле этого получится бинарь, который можно запускать так:
target/release/examples/define test.jsЭто будет значительно быстрее, чем первоначальный cargo run.
Замеряем скорость
Дальше будет очень много условностей, но какие-то представления получить можно. Методика не выдерживает никакой критики, я знаю. Я не ставил целью получить на 100% точные данные, я просто хочу посмотреть порядок чисел и представление о разных подходах (в том числе теоретически достижимые).
Ради примера я взял не минифицированный реакт с react-dom общим весом около 1.2 МБ.
Итого есть 3 реализации плагина: на Babel, SWC и OXC.
Для прогона SWC я взял 2 подхода: без плагинов и с плагином (чтобы оценить оверхед от плагина).
Для OXC я сделал аналогичное: взял пакет oxc-transform из npm (понятное дело, без плагина) и собранный бинарь из прошлого раздела с плагином.
В каждом случае я сделал 100 прогонов одного и того же файла. Все прогоны внутри, файл читался 1 раз.
Соответственно, все прогоны работают через Node.js, кроме героя данной статьи. Его я взял за 100%:
| Относительно | Время | |
|---|---|---|
| OXC+plugin | 1x | 1.552s |
| OXC Node | 1.28x | 1.98s |
| SWC | 4.35x | 6.753s |
| SWC+plugin | 8.4x | 13.034s |
| Babel 8 | 13.27x | 20.599s |
| Babel 7 | 20.57x | 31.923s |
Несмотря на всю кривизну прогона, сравнение SWC vs OXC в общем-то сходится с официальными бенчмарками OXC, в которых он примерно в 4 раза быстрее SWC.
Ещё одно интересное наблюдение про запуск OXC через Node.js, который медленнее на 28%, чем запуск нативного бинаря. Понятно, что Node.js требует времени на запуск, но тут явно что-то ещё. Передача строк между бинарным кодом и Node.js? Вероятно.
SWC c плагином почти в 2 раза медленнее, чем без плагина. А вот это сюрприз. Видимо, передача данных между Rust <-> WASM имеет большой оверхед по итогу. OXC, в котором всё внутри Rust, такой проблемы не имеет.
Babel всего в 1.58 раз медленнее, чем SWC с плагином. Я ожидал куда большего разрыва. Без плагина получаем около 3х раз. Вспоминая всякие посты про ускорение в 20-70 раз от SWC как-то… Грустно.
С другой стороны, всё это из-за сильного ускорения Babel в новом мажорном релизе. Примерно в 1.5 раза в таком примитивном сравнении – очень здорово. Будет ли такое ускорение в реальных кейсах ещё большой вопрос, но повод обновиться так точно.
Ну и нельзя не забыть о том, что OXC с плагином тяжело запустить из JS кода. Какое-то время уйдёт на работу обёрток, поэтому в реальности будет чуть хуже.
Итого
Результаты для меня несколько неожиданные. Если вы используете только стандартные трансформации, и прямо сейчас у вас Babel, то Esbuild/SWC/OXC дадут вам ускорение в разы, а в случае OXC – примерно в 10 раз. а то и больше.
С другой стороны, по моей практике время чаще уходит на минификацию / tsc (в рамках ts-loader, например), и уже потом Babel становится основным потребителем. Оцените сначала время вашей сборки, разделите составляющие, и, возможно, найдётся что-то более важное, чем Babel.
Теперь к кастомным трансформациям. Мы рассмотрели 3 инструмента и плагины к ним:
- Babel. Просто и надёжно. Плагины пишут давно, куча док. А с 8й версией даже не так медленно, как можно подумать. Код переносимый, не требует сборки, удобно таскать рядом с проектом без каких-либо проблем.
- SWC. Дока есть, но не очень актуальная. Проблемы с поломками бинарного апи при выходе новой версии (теоретические). Бинарная сборка Rust в WASM. Тут, конечно, плюс, что сам SWC поставляется в собранном виде для разных платформ, а WASM заработает в разных архитектурах процессоров и в разных ОС. Сам подход выходит за пределы привычного
npm installи фронтендерского стека. Обычно не будешь заставлять разработчиков интерфейсов ставить Rust, тут нужна какая-то автоматизация по сборке WASM. Но суммарно работает быстрее, чем Babel. - OXC. Заметно быстрее SWC. Очень ограниченная документация. Формат “собери себе сам”. И ты не можешь просто собрать пакет, тебе нужны бинари под разные архитектуры (особенно, если разработчики могут быть на разном железе, а в CI вообще ещё что-то третье). Публикация получается заметно сложнее, чем в случае с SWC, и, тем более, с Babel.
Итого имеем понятную кривую: хочется просто и дёшево - берём Babel. Заметно сложнее и сколько-то быстрее – SWC. Хотим сделать максимально быструю сборку и у нас есть много ресурсов для этого – идём копать OXC.
Какой итог по самому OXC? Впечатляюще. Очень. При этом разработчики пишут, что они старались в совместимость с тестами Babel, а без них было бы ещё лучше. Использовать сейчас для кастомного плагина тяжело, но результаты могут того стоить. Ждём публичное апи для плагинов? Возможно, оно тоже будет в WASM.
А пока может выглядеть чужеродно для “обычного” фронтенд-проекта. Или привыкнем?
Обсудить в Telegram



