はじめに
最近web開発の統合ツールチェインとしてアルファ版が公開されたばかりのVite+ですが、やたらとフォーマット・リンターが遅いなぁと感じていました。はじめに申し上げますと、原因は分かったんですが解決は出来ていない状況です。
なんか遅いなと感じていた
Vite+を使用する前は、Biomeというフォーマッター・リンターを使用していました。別段遅いと感じたことはなく、やっぱRust早いなぁと弱々プログラマーらしくRustの権威に頭をひれ伏していました。
Vite+に乗り換えて...
vp migrateで既存のコードベースに沿った移行作業をしてくれます。移行作業を終え、とりあえず試しでフォーマットとリンターをまとめて行ってくれるvp checkを実行しました。
で実行時間は?
❯ vp check
VITE+ - The Unified Toolchain for the Web
pass: All 114 files are correctly formatted (1798ms, 16 threads)
pass: Found no warnings, lint errors, or type errors in 61 files (2.1s, 16 threads)
え?遅くね? 114ファイルを16 threadsで 2秒かかってんだけど。Rust使ってるんだよね?
プラス、git hookにvp checkが登録されているので、コミットするたびに2秒の待ち時間が発生します。
時間がかかっている箇所の調査
とりあえず実測
vp checkは中でoxfmtとoxlintをラップして実行します。切り分けとしてフォーマットの方を調査していくことにしました。
vp fmtを実行することで、フォーマットオンリーで実行できます。oxfmtを直接実行とどれくらい差分があるかを確認してみました。
❯ vp fmt test.ts
VITE+ - The Unified Toolchain for the Web
Finished in 1282ms on 1 files using 16 threads.
❯ ./node_modules/.pnpm/node_modules/.bin/oxfmt test.ts
Finished in 75ms on 1 files using 16 threads.
一目瞭然ですね。比べるまでもないです。
単一ファイルに1秒以上かかっているこの状況なんなんだ、ということで調査をしました。
Claude Codeに調査をぶん投げる
地道な確認作業やデバックなんか一切してないです。味気なくてすみません。
次のステップは Claude Codeがどういう思考回路で動いたか、その作業ログになります。
vp fmt のアーキテクチャを把握する
まず vp fmt が内部で何をしているか、プロセスツリーを確認しました。vp fmt は数秒かかるので、バックグラウンドで起動して子プロセスが立ち上がったタイミングで ps を叩きます。
❯ vp fmt test.ts & sleep 0.5 && ps --forest -o pid,args -g $$
PID COMMAND
vp fmt test.ts
\_ /home/.../.vite-plus/js_runtime/node/24.14.0/bin/node .../vite-plus/dist/bin.js fmt test.ts
vp(Rust バイナリ)は Node.js を子プロセスとして起動し、dist/bin.js に処理を委譲していました。つまり実行フローはこう
vp (Rust binary)
→ Node.js (dist/bin.js)
→ NAPI binding (Rust)
→ oxfmt (子プロセス)
レイヤーを一枚ずつ剥がして差分を取る
プロセスが多段になっているので、外側のレイヤーから順にスキップして実行し、各レイヤーのコストを差分で割り出しました。
# A. vp fmt(全レイヤー)
❯ /usr/bin/time -v vp fmt test.ts
Elapsed: 0:02.76
# B. Node.js bin.js を直接実行(Rust ランチャーをスキップ)
❯ /usr/bin/time -v node dist/bin.js fmt test.ts
Elapsed: 0:02.75
# C. oxfmt 直接(Node.js + bin.js もスキップ)
❯ /usr/bin/time -v oxfmt test.ts
Elapsed: 0:00.17
| 比較 | 差分 | 意味 |
|---|---|---|
| A - B | 0.01s | Rust ランチャーのコスト → ほぼゼロ |
| B - C | 2.58s | bin.js 内部の処理コスト → ここがボトルネック |
bin.jsが圧倒的に処理コストの中心となっていることが確認できます。
bin.js の中で何が遅いのか
bin.js の内部では、NAPI binding 経由で複数の JS resolver 関数が呼ばれます。各 resolver にタイマーを仕込んだところ、犯人は一発で特定できました。
[resolver] fmt(): 0 ms
[resolver] resolveUniversalViteConfig(): 1318 ms ← 犯人
run() total: 2758 ms
resolveUniversalViteConfig() が 1.3 秒。Vite+ のソースを読むと、この関数は vite.config.ts から fmt プロパティを取得するために Vite の resolveConfig() をフル実行していました。fmt の設定を読みたいだけなのに、vite.config.ts に書かれた全プラグイン 82 個を import・初期化している。
import { defineConfig } from "vite-plus";
const config = defineConfig({
fmt: { // こいつを読みたいだけなんです...
ignorePatterns: ["src/routeTree.gen.ts", "src/styles.css", "docs/**"],
printWidth: 80,
tabWidth: 2,
useTabs: false,
singleQuote: false,
trailingComma: "all",
},
plugins: [ // ここに書かれたプラグインが初期化されてしまう...
devtools(),
alchemy(),
tailwindcss(),
tanstackStart(),
viteReact(),
babel({ presets: [reactCompilerPreset()] }),
],
});
export default config;
裏を取るため vite.config.ts の plugins を空にして再計測をしてみました。
# プラグインあり (82個)
❯ /usr/bin/time -v vp fmt test.ts
Elapsed: 0:02.76
# プラグインなし
❯ /usr/bin/time -v vp fmt test.ts
Elapsed: 0:00.56
プラグインの初期化が行われることで、処理が遅くなっていることが確認できました。
原因まとめ
原因は、vp fmtを実行したとき、fmtプロパティをvite.config.tsから取得するとセットでプラグインまで初期化されてしまうものでした。
-
vp fmtはvite.config.tsからfmt設定を読むために Vite のresolveConfig()をフル実行する - プラグインの import と初期化を含むため、ビルド用プラグインが多いプロジェクトほど遅くなる
-
fmt/lintの設定だけ取りたいのにビルドパイプライン全体をロードする設計になっている
対策
現時点では無いです
Vite+ を使う以上避けられないので、改善されるのを待つしかないです。
アルファ版だから仕方なしっていう感じなですかね?あんまりVite+に対する接し方がわからないです。