開発環境のLinterとFormatterを、ESLint・PrettierからOxlint・Oxfmtに引っ越した。というのも、push前のチェックがじわじわと待ち遠しくなってきたからだ。

このプロジェクトでは、Huskyでpush前にLintと型チェックを走らせている。AIで開発するようになってから、コードを直してpushするまでの回転がぐんと速くなった。回転が速くなるほど、pushのたびに挟まるこの待ち時間が目につくようになる。

pushのたびにLintを走らせるのなら、せめて秒で終わってほしい。そこで、以前から気になっていたOxlintとOxfmtに重い腰を上げたわけだ。

OxlintとOxfmt

OxlintとOxfmtは、Oxcプロジェクトが開発しているJavaScript・TypeScript向けのツールだ。それぞれ、おなじみのツールの代わりを務める。

従来移行先用途
ESLintOxlint静的コード解析
PrettierOxfmtコードフォーマット

OxcはRustで実装されていて、速さを売りにしている。もっとも、速いという評判は耳にしていたものの、自分のプロジェクトでどれほど変わるのかは見当がつかなかった。

今回の舞台は、pnpmのモノレポだ。apps/ の下に、NestJSで組んだバックエンド、Reactのフロントエンド、そして管理画面やヘルプページ、社内ツールといった小ぶりなアプリがいくつか並んでいる。これらのアプリで、それぞれのESLint設定をOxlintに移してみた。

Lintの引っ越し

結果は次の通りになる。いずれも実際のプロジェクトで測った値で、合計は各アプリの時間を単純に足したものだ。

対象ESLintOxlint高速化
Backend約33秒約4.6秒約7倍
Frontend約15秒約0.4秒約37倍
その他のアプリ約12秒約0.5秒約24倍
合計約60秒約5.5秒約11倍

なお、BackendのOxlintの時間には、後述する命名規約のチェック(こちらは引き続きESLintで走らせている)を含めていない。

とりわけ効いたのがフロントエンドだ。ESLintで15秒ほどかかっていた処理が、Oxlintでは0.4秒で終わる。初めて実行したときは、あまりにあっけなく終わるので、本当にチェックしているのかと疑ったほどだ。もちろん、ちゃんと仕事はしていた。

バックエンドでも約7倍速くなっている。push前の待ち時間を削るという目的からすれば、これだけでも十分におつりがくる。

旧居に残した家具

ただし、ESLintを完全に放逐したわけではない。引っ越しでも、新居に入らない家具はしばらく旧居に置いておくものだ。

バックエンドはClean Architectureで組んでいて、依存関係と命名に独自の制約を設けている。依存関係のほうは、Application層からTypeORMを直接インポートしない、Infrastructure層からHTTP例外を投げない、ControllerからInfrastructure層を直接参照しない、といったルールだ。これらはOxlint側でチェックするようにした。

一方、命名規約にはESLintの no-restricted-syntax を使っていた。たとえばUseCaseの名前には、次のような縛りを設けている。
避けたい名前推奨する名前
SaveCreate / Update
EditUpdate
FetchGet / List
Repositoryも同様で、曖昧な save や edit ではなく、create や update を使わせている。今回の移行では、この命名規約のチェックだけをESLintに残した。そのため、バックエンドのLintはOxlintとESLintの二本立てで走っている。

一度に全部を運び出そうとするより、Oxlintで賄える部分だけを移し、独自ルールはそのまま生かしておくほうが穏当だと判断した。なお、OxlintにはJavaScriptプラグインでESLintのルールを再利用する仕組みもある。早晩、命名規約のチェックもOxlintに統合できるかもしれない。

引っ越しのついでに掃除はしない

移行では、既存コードとの折り合いもつける必要があった。バックエンドでは、今回入れたアーキテクチャ制約に違反しているディレクトリが2つ見つかった。Application層のサービスの1つがTypeORMを直接使っていて、Infrastructure層のサービスの1つがHTTP例外を投げていたのだ。

どちらも既存の実装なので、今回はチェックの対象から外している。フロントエンドでもReact関連の警告がいくつか出たが、こちらも警告扱いにとどめ、別のタスクで片づけることにした。

Lintツールを替えることと、既存コードをリファクタリングすることは、まったく別の作業である。引っ越しの日に押し入れの大掃除まで始めると、たいてい日が暮れる。変更範囲を膨らませないために、今回は移行だけを優先した。

Formatterも引っ越す

Oxlintが想像以上に速かったので、勢いでFormatterも替えることにした。Prettierの代わりにOxfmtを据える。各アプリに .oxfmtrc.json を置き、package.json のformatコマンドを書き換えた。

そのうえで、バックエンドの2,218ファイルを対象に実行時間を比べた。

ツール実行時間高速化
Prettier7.5秒-
Oxfmt82ms約91倍

7.5秒が82ミリ秒になった。これにはさすがに面食らった。

Prettierを普段使っていて、極端に遅いと感じていたわけではない。それでも、約2,200ファイルが0.1秒とかからずに片づくのを見ると、違いは歴然としている。ここまで速ければ、ファイル数の多いプロジェクトでもフォーマットチェックを気軽に回せる。

むろん、Formatterを替えれば出力に差が出る可能性はあるので、既存の設定との互換性は確かめておく必要がある。今回は、移行後に format:check が正常に動くところまで確認した。

新居の間取り

最終的な構成は次の通りになった。

対象LinterFormatter
BackendOxlint + ESLint(命名規約のみ)Oxfmt
FrontendOxlintOxfmt
その他のアプリOxlintOxfmt
使ったバージョンは oxlint@^1.82.0 と oxfmt@^0.67.0 だ。ルートの package.json にもコマンドを足し、モノレポ全体を一度に検められるようにしてある。
# 全アプリのLint
pnpm lint

# 全アプリのフォーマット
pnpm format

# フォーマットチェック
pnpm format:check

最後に通しで確認したところ、Lintもフォーマットチェックも問題なく動いた。

Huskyと組み合わせる

冒頭に書いた通り、このプロジェクトではHuskyの pre-push フックで次の処理を走らせている。
pnpm lint
pnpm typecheck

これで、push前に静的解析と型チェックが行われる。CIの代わりになるわけではないが、問題のあるコードをpushの手前で止められるぶん、CIの無駄な失敗は減らせる。そして、LintをOxlintに移したことで、このチェック自体も速くなった。

ただし、Huskyの導入でGitHub Actionsの実行回数がどれだけ減ったのかは、まだ数字で測れていない。今回確かめられたのは、あくまでローカルで走らせるLintとFormatterが速くなったことだけである。

速さは道具選びの全部ではない

以上、ESLint・PrettierからOxlint・Oxfmtへの引っ越しの顛末を書いた。実測では、Lintがバックエンドで約7倍、フロントエンドで約37倍、その他のアプリで約24倍、フォーマットがバックエンドで約91倍速くなった。正直なところ、ここまで差が出るとは思っていなかった。

もちろん、速さだけで道具を選ぶべきではない。ESLintには豊富なプラグインがあり、独自ルールを柔軟に組める強みがある。現に僕も、命名規約のチェックはESLintに任せたままだ。それでも、標準的なLintルールとフォーマットを高速化できる恩恵は大きい。

AIを使った開発では、コードを生成し、直すまでのサイクルがぐんと速くなった。そのぶん、Lintや型チェック、テストにかかる数秒、数十秒が、開発のテンポを崩す原因になりうる。コードを書く時間が縮んだからこそ、その周りにある道具も見直す値打ちがあるのだ。