プラグインシステムはパフォーマンスへの課税
VS Code をインストールします。速いです。15個の拡張機能を追加します。すると起動に4秒かかるようになり、 Extension Host が 800 MB の RAM を消費します。何が起きたのでしょうか?
このパターンはいたるところで繰り返されます:WordPress、Eclipse、Chrome、Figma、Slack。アプリは 速くリリースされます。プラグインが遅くします。もう誰も驚きません — 拡張性のコストとして 受け入れてしまっています。
しかし、プラグインシステムは単なるパフォーマンスの問題ではありません。設計哲学の問題です。 業界は「拡張性」を「ランタイムの動的性」と混同していますが、より良い答えはコンパイル時の 合成であることが多いのです。唯一パフォーマンスの良いプラグインは、コンパイル時にプラグインで なくなるものです。
拡張性のパフォーマンススペクトラム
すべての拡張性が同じコストではありません。ゼロコストから最大コストまでのスペクトラムがあり、 業界の大部分はコストの高い側に定着しています:
- 静的リンク/コンパイル時モジュール — オーバーヘッドゼロ。C ライブラリ、 Rust クレート、Go パッケージ。モジュール境界は最終バイナリで完全に消滅します。
- 起動時にロードされる共有ライブラリ — ほぼゼロ。nginx モジュール、 Linux カーネルモジュール。ロード時の一回限りのコストで、その後は直接関数呼び出し。
- インターフェース/vtable を介した動的ディスパッチ — 小さなオーバーヘッド。 C++ のゲームエンジンプラグイン。呼び出しごとに1つのポインタ間接参照。
- 同一プロセス内のインタプリタプラグイン — 中程度。WordPress の PHP プラグイン、 Eclipse の OSGi バンドル。すべてのプラグイン呼び出しがインタプリタを経由。
- IPC を介した別プロセスプラグイン — 大きい。VS Code 拡張機能、 Chrome 拡張機能。すべてのインタラクションがプロセス境界を越えてデータをシリアライズ。
- シリアライズド IPC を介したサンドボックスプラグイン — 重い。Figma プラグイン、 ブラウザ拡張機能のコンテントスクリプト。すべての呼び出しでシリアライズ、デシリアライズ、 サンドボックス適用。
重要な洞察:パフォーマンスの良いプラグインは、コンパイル時にプラグインでなくなるものだけです。 レベル 1 と 2 が速いのは、「プラグイン」が最終アーティファクトでホストコードと 区別できなくなるからです。
現実世界での被害
WordPress
すべてのプラグインがリクエストライフサイクルにフックします。30個のプラグインはページロードあたり 30層の関数呼び出しを意味します。結果:他のプラグインの被害を軽減するためだけに キャッシュプラグインが存在します。プラグインが作り出したパフォーマンス問題を修正する パフォーマンスプラグイン。メタ的な皮肉です。
VS Code
拡張機能は別プロセスの単一の Node.js イベントループを共有します。1つの問題のある拡張機能が 他のすべてをブロックします。Extension Host は定期的に開発者のマシンで CPU 消費量トップに 表示されます。Microsoft はプロファイリングツール、bisect コマンド、アクティベーションイベント システムを構築しました — 拡張機能が作り出す問題を管理するための完全なインフラストラクチャです。
Eclipse
警告的な物語です。OSGi バンドルの解決、クラスローディングのオーバーヘッド、巨大な依存関係グラフ。 かつて最も人気のある IDE でしたが、今ではメインストリームの開発者に大部分放棄されています。 最大の強みとなるはずだったプラグインアーキテクチャが、決定的な弱点になりました。
Electron そのもの
プラットフォームレベルのプラグイン問題です。すべての Electron アプリが完全な Chromium + Node.js ランタイムを含みます。VS Code は Electron。Slack は Electron。Discord は Electron。それぞれが独立して 300~500 MB の RAM を消費して、本質的にはチャットウィンドウや テキストエディタをレンダリングしています。ここでの「プラグイン」はウェブプラットフォーム全体で、 アプリケーションごとに新たにバンドルされています。
なぜ業界はプラグインを選び続けるのか
プラグインがそれほどコスト高なら、なぜ皆が作り続けるのか?理由は主に組織的であり、 技術的ではありません:
- 開発者体験 — パフォーマンスを気にしなければ、プラグインは書きやすいです。 JS ファイルを出荷し、イベントにフックして完了。
- エコシステムの成長 — プラグインはネットワーク効果とコミュニティの エンゲージメントを生み出します。30,000 の拡張機能のマーケットプレイスは強力な堀です。
- 組織的な便利さ — プラグインはチームが設計決定を先延ばしにすることを可能にします。 「誰かがそれ用のプラグインを書くだろう」はアーキテクチャ版の 「ポストプロダクションで直す」です。
- ビジネスモデル — プラグインマーケットプレイスは収益とロックインを生み出します。 プラットフォームがエコシステムから価値を獲得します。
不都合な真実:プラグインは多くの場合、コアに何が属するかについての難しい アーキテクチャ上の決定を避ける方法です。不完全なものをリリースして 「拡張可能」と呼ぶことを可能にします。
代替案:コンパイル時合成
拡張性がランタイムではなくビルド時に起こるとしたら?
これは仮説ではありません。システム言語で実証済みの前例があります:
- Rust の proc マクロ — コンパイル時に実行され、ゼロオーバーヘッドのネイティブコードを 生成する任意のコード。Serde のシリアライゼーション、Tokio の async ランタイムセットアップ、 Axum のルーティング — すべてプログラム開始前に解決されます。
- Zig の comptime — すべてのランタイム分岐を排除するコンパイル時実行。 ジェネリックデータ構造は単相化され、設定は解決され、デッドコードは排除されます。 残るのは実行されるものだけです。
- C++ テンプレート / constexpr — ランタイムコストゼロのコンパイル時ポリモーフィズム。 STL がすべてのジェネリックアルゴリズムをコンパイル時に特殊化するため、 並外れたパフォーマンスを達成しています。
- バンドラーのツリーシェイキング — JavaScript に適用されたこのアイデアの 部分的で不完全なバージョン。Webpack と Rollup がビルド時に未使用のエクスポートを排除します。 制限は、コードを削除できるだけで特殊化できないことです。
パターンは一貫しています:決定をランタイムからビルド時に移動させること。 含めないものはコストがかかりません。含めるものは間接参照なしのネイティブコードにコンパイルされます。 モジュール境界はソースレベルの整理ツールとなり、ランタイムのパフォーマンス境界ではなくなります。
TypeScript にとっての意味
TypeScript は拡張可能なツールを構築するための最も人気のある言語であり、 ランタイムパフォーマンスでは最悪です。TypeScript エコシステム全体が Node.js 上で動作し、 Node.js は V8 上で動作し、V8 は JavaScript を JIT コンパイルします。すべてのレイヤーが オーバーヘッドを追加します:JIT ウォームアップ時間、ガベージコレクションの一時停止、 すべてのプロパティアクセスでの動的ディスパッチ、プロセス間の IPC 境界。
ここで Perry の出番です。Perry は TypeScript を直接ネイティブバイナリにコンパイルします。 V8 なし、JIT ウォームアップなし、ガベージコレクションの一時停止なし、IPC 境界なし。
モジュールがネイティブコードにコンパイルされると、「プラグイン」は単なる...モジュールになります。 ビルド時に合成されます。最終バイナリにはプラグインのオーバーヘッドがゼロです。 プラグインが存在しないからです — ネイティブコードだけです。Express のルートハンドラ、 ミドルウェア関数、ユーティリティライブラリ — すべて同じバイナリ内の直接関数呼び出しに コンパイルダウンされます。動的ローディングなし、シリアライゼーションなし、プロセス境界なし。
# Your app, your dependencies, your "plugins" — one binary
$ perry compile server.ts -o server
Compiling server.ts + 43 modules...
✓ Built executable: server (1.8 MB, 0.7s)
$ ./server
Listening on port 3000
これは理論的な話ではありません。Perry はすでに実世界の TypeScript フレームワーク — Hono、tRPC、 Strapi — を 2 MB 未満のネイティブ ARM64 バイナリに1秒未満でコンパイルしています。 これらのフレームワークを構成するモジュールはコンパイルされ、リンクされ、 単一の実行ファイルにインライン化されます。Node.js ではランタイムオーバーヘッドのある プラグインアーキテクチャになるものが、Perry バイナリではゼロコストの合成になります。
実際に必要な拡張性
反論は明白です:「でもランタイムの拡張性が必要です。ユーザーが再コンパイルせずに プラグインをインストールできる必要があります。」
本当にそうですか?ほとんどのアプリケーションでは、拡張のセットはビルド時に既知です。 Express ミドルウェア、データベースドライバー、認証ライブラリ、ロギングフレームワークを 選択してからデプロイします。「拡張性」は package.json にあり、 npm install 時に解決されます。ランタイムではありません。
本当にランタイムプラグインローディングが必要なアプリケーション — VS Code、WordPress、 ブラウザ — は例外であり、ルールではありません。そしてそれらでさえ大きな代償を払っています。 それ以外のすべてについて、コンパイル時の合成は同じ柔軟性をオーバーヘッドなしで提供します。
違いはアーキテクチャの正直さです。すべてのアプリケーションにプラグインシステムが 必要だと装う代わりに問いかけます:この拡張性はランタイムで起こる必要があるか、 それともコンパイラが仕事をできるか?
今後の道
業界のプラグインアーキテクチャへの依存は、ランタイムオーバーヘッドを不可避として 受け入れることの症状です。不可避ではありません。コンパイラが仕事をできます。 ビルド時の合成は税金なしの拡張性を提供します。
TypeScript 開発者が愛する言語を諦めることなくネイティブパフォーマンスに値すると信じて Perry を構築しています。モジュールはビルド時に合成され、直接関数呼び出しにコンパイルされ、 「拡張性」を可能にするためだけに存在するランタイムのオーバーヘッドなしに実行されるべきです。
最速のプラグインシステムは、ランタイムに存在しないものです。