Rust Web開発コンパイルの高速化
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
Rustはその比類なきパフォーマンス、メモリ安全性保証、堅牢な型システムにより、Web開発において急速に注目を集めています。Actix-web、Axum、Warpなどのフレームワークは、高性能なWebサービスを構築するための強力なツールを提供します。しかし、 nontrivial なRust Webアプリケーションに取り組んだ開発者なら誰でも、すぐに重大な障害に直面するでしょう。それはコンパイル時間です。最初の完全コンパイルは永遠のように感じられることがあり、インクリメンタルビルドでさえもどかしいほど遅く、開発フィードバックループを著しく損なう可能性があります。この生来の特性は、特にコンパイルサイクルが速い言語や動的インタープリタから来た人々にとって、疑問や懸念を引き起こすことがよくあります。「なぜ」コンパイルが遅いのかを理解することは、それを効果的に軽減するための最初の一歩であり、最終的にはより生産的で楽しいRust Web開発体験につながります。この記事では、Rustのコンパイルの遅さの理由を掘り下げ、さらに重要なことに、開発ワークフローを最適化するための実践的なツールとテクニックを探ります。
Rust Webコンパイルの解読
ソリューションを深く掘り下げる前に、Rustのコンパイルプロセスを理解するために不可欠ないくつかのコアコンセプトを明確にしましょう。
- コンパイル: 人間が読めるソースコードを機械実行可能なバイナリコードに変換するプロセスです。RustはAhead-of-Time(AOT)コンパイル言語であり、このステップは実行前に行われます。
 - インクリメンタルコンパイル: Rustコンパイラの機能であり、最初からすべてを再構築するのではなく、前回正常にコンパイルされた後で変更されたコードの部分のみを再コンパイルしようとします。これにより、後続のビルドが大幅に高速化されます。
 - リンカー: コンパイラ(オブジェクトファイル)からの出力を受け取り、コードの異なる部分間の参照を解決して、単一の実行可能ファイルに結合するプログラムです。これは、完全なビルドの最も遅い部分であることがよくあります。
 - Codegen Backend: 実際の機械コードの生成を担当するコンパイラの一部です。Rustは主にLLVMをCodegen Backendとして使用します。
 - 依存関係グラフ: プロジェクト内の異なるモジュール、クレート、ライブラリ間の関係のネットワークです。基盤となる依存関係の変更は、それに依存するすべてのものの再コンパイルを引き起こす可能性があります。
 
Rust Webアプリケーションが遅くコンパイルされる理由
Rustの安全性とパフォーマンスへのコミットメントは、いくつかの理由でコンパイル時間の長期化に本質的に寄与しています。
- 厳密な借用とライフタイム: 借用チェッカーは、ガベージコレクタなしでメモリ安全性を保証するために広範な静的解析を実行します。この解析は、特に大規模なコードベースやWebアプリケーションロジックで一般的に見られる複雑なデータ構造の場合、複雑で計算負荷が高いです。
 - ジェネリクスのモジュロ化: Rustのジェネリクスはモジュロ化されます。つまり、コンパイラは、それが使用される各具体的な型に対してジェネリック関数または構造体のユニークなバージョンを生成します。これにより実行時オーバーヘッドは排除されますが、コンパイラが処理および最適化する必要のあるコードの量が増加する可能性があります。Webフレームワークは、リクエストハンドラ、ミドルウェア、データ型のためにジェネリクスを頻繁に重用します。
 - 広範な最適化: Rustc、Rustコンパイラは、LLVMを活用して積極的な最適化を実行し、非常に効率的な機械コードを生成します。これらの最適化は、パフォーマンスにとって重要ですが、時間がかかる場合があります。
 - マクロ展開: Rustの強力な宣言型および手続き型マクロは、コンパイル時にかなりの量のコードを生成できます。Actix-webのようなWebフレームワークは、ルーティング、ハンドラ属性定義、トレイトの導出のために手続き型マクロに大きく依存しており、コンパイル負荷が増加します。
 - 大規模な依存関係ツリー: Webアプリケーションは、JSONシリア ライゼーション、データベース操作、認証、ロギングなどのタスクのために多数のクレートをプルすることがよくあります。これらの各依存関係はコンパILされ、それ自体の推移的依存関係はコンパILグラフをさらに拡大します。一般的なユーティリティクレートの小さな変更でさえ、広範な再コンパILを引き起こす可能性があります。
 - I/Oとリンカーのパフォーマンス: 最終的なリンクステージ、特にWindowsではボトルネックになる可能性があります。リンカーは、生成されたすべてのオブジェクトファイルを単一の実行可能ファイルに結合する必要があります。これはI/OバウンドでCPU集約的なプロセスです。
 
Rust Web開発ワークフローの最適化
RustのコンパIL特性は固有のものですが、開発者体験を大幅に改善するための強力なツールと戦略があります。
1. cargo-watchの力
コード変更ごとに cargo run または cargo build を繰り返し入力するのは非効率的です。cargo-watch は、ソースコードの変更を検出すると自動的にアプリケーションを再コンパILして再実行する不可欠なツールです。
インストール:
car go install cargo-watch
使用例:
基本的なAxum Webアプリケーション構造を想定します。
src/main.rs:
use axum:: routing::get, Router, ; #[tokio::main] async fn main() { // 単一のルートでアプリケーションを構築します let app = Router::new().route("/", get(handler)); // `localhost:3000`でハイパーで実行します let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } async fn handler() -> &'static str { "Hello, Axum Web!" }
cargo run の代わりに、次を使用します。
car go watch -x run
このコマンドはプロジェクトを監視し、.rs ファイル(または設定された他のファイル)を保存するたびに cargo run を実行します。
高度なcargo-watch設定:
- コマンドの指定: 
cargo watch -x 'run --bin my_app arg1' - 画面クリア: 
cargo watch -c -x run(実行ごとにターミナルをクリアします) - 間隔: 
cargo watch -i 1000 -x run(1000msごとにチェックします。デフォルトは500msです) - コマンド後: 
cargo watch -x 'clippy --workspace' -s 'echo Clippy finished'(clippyを実行し、メッセージを表示します) 
cargo-watch は、編集-コンパイル-テストのサイクルを劇的に短縮し、開発がよりスムーズに感じられるようにします。
2. sccache による分散キャッシュの活用
sccache はMozillaによって開発されたコンパイルキャッシュツールであり、中間のビルド成果物を保存し、同じコンパイル入力が再度検出されたときに再利用することで、再コンパイルを大幅に高速化できます。これは、多数の依存関係を持つ大規模なプロジェクトや、ブランチを切り替える場合に特に効果的です。
インストール:
car go install sccache --features=native
設定:
cargo がデフォルトで sccache を使用するようにするには、RUSTC_WRAPPER 環境変数を設定する必要があります。
Linux/macOS:
echo "export RUSTC_WRAPPER=$(rg sccache)" >> ~/.bashrc # または ~/.zshrc source ~/.bashrc
Windows PowerShell:
[System.Environment]::SetEnvironmentVariable('RUSTC_WRAPPER', (Get-Command sccache).Source, 'User')
これを設定した後、cargo は自動的に rustc の前に sccache を呼び出します。
sccache の仕組み:
sccache が有効になっている場合:
rustcへの呼び出しをインターセプトします。- コンパイルコマンド、ソースコード、およびその他の入力をハッシュします。
 - そのハッシュに対応するキャッシュされた出力が既に存在するかどうかを確認します。
 - ヒットした場合、キャッシュからコンパイル済み出力を取得します。
 - ミスの場合、
rustcをローカルで実行し、出力をキャッシュに格納してから返します。 
sccache は、クラウドストレージバックエンド(AWS S3、Google Cloud Storage)を使用した分散キャッシュ用に設定することもでき、CI/CDパイプラインや大規模チームに役立ちます。
sccache の統計を確認するには:
sccache --show-stats
3. Cargo を高速ビルド用に最適化する
Cargo 自体は、ビルド時間を改善するためのさまざまな設定オプションを提供しています。
3.1. 高速リンカー
リンカーはボトルネックになる可能性があります。Linux では、lld(LLVM のリンカー)は GNU ld または gold よりもはるかに高速であることがよくあります。
インストール(Ubuntu/Debian):
sudo apt install lld
設定(プロジェクトルートまたは ~/.cargo/config.toml の .cargo/config.toml):
[target.x86_64-unknown-linux-gnu] # ターゲットトリプルを必要に応じて調整します linker = "clang" # または "ld.lld" rustflags = ["-C", "link-arg=-fuse-ld=lld"] # 高速なデバッグビルド(特にcargo-watchとの併用に便利) [profile.dev] opt-level = 1 # デバッグビルドでいくつかの最適化を有効にします debug = 2 # デバッグ情報を保持します lto = "fat" # リンク時最適化(遅くなる可能性がありますが、役立つこともあります) codegen-units = 1 # 最大最適化のために、ビルド時間を増加させます。高速ビルドにはより高い値をお勧めします。 [profile.dev.package."*"] codegen-units = 256 # デバッグビルドを高速化するために、依存関係のcodegen-unitsを高くします。
Windows では、mold は検討すべきもう一つの高性能リンカーです。
3.2. デバッグ情報の削減
デフォルトでは、デバッグビルドには広範なデバッグ情報が含まれており、コンパIL時間とバイナリサイズが増加します。これは特定の問題をデバッグするために不可欠ですが、一般的な開発では削減できます。
.cargo/config.toml で:
[profile.dev] debug = 1 # デバッグ情報を削減します(デフォルトの2から)。バックトレースにはまだ使用できます。 # debug = 0 はデバッグ情報を完全に削除し、最も高速ですが、デバッグ可能性は最も低いです。
3.3. 並列処理の最大化
Cargo は、依存関係と翻訳単位を並列でコンパILできます。
.cargo/config.toml で:
[build] jobs = 8 # CPUコア数、またはそれより少し多い数に設定します(デフォルトは多くの場合、システム依存のヒューリスティックです)。
ただし、jobs を増やしすぎると、I/O競合やメモリ不足のためにパフォーマンスが悪化する場合があることに注意してください。最適な値を見つけるために実験してください。
3.4. 依存関係の事前コンパイル
大規模なプロジェクトでは、一般的な依存関係が変更されることはほとんどありません。デバッグビルドで依存関係が変更されなかった場合に、リリースモードでそれらを事前コンパILすると、後続のデバッグビルドが高速化されます。
car go build --release --workspace # すべてのクレートをリリースモードでビルドします
これにより、.cargo/target/release が populat され、rustc は後でそれにリンクできる可能性があります。
4. コード構造と設計の選択
ツールを超えて、Rust Webアプリケーションの構造方法もビルド時間に影響を与える可能性があります。
- より小さなクレート: 大規模なアプリケーションをより小さく、より焦点を絞ったクレート(例:
my_app_api、my_app_domain、my_app_utils)に分割すると、インクリメンタルビルド時間が改善される可能性があります。my_app_apiの変更が、API自体が変更されていない限り、必ずしもmy_app_domainの再コンパILを必要としません。 - トレイトオブジェクト(動的ディスパッチ)を念頭に置く: 静的ディスパッチ(ジェネリクス)はゼロコストですが、動的ディスパッチ(トレイトオブジェクト、例:
Box<dyn MyTrait>)はモジュロ化を回避します。パフォーマンスが絶対的に重要でない場所でのトレイトオブジェクトの慎重な使用は、コンパイラが処理する必要のあるコード量を減らすことができます。ただし、これはトレードオフであり、必ずしも注意深い設計なしに全体的.なコンパILが速くなるとは限りません。 - ジェネリクスの最小化: ジェネリック型が1つまたは2つの具体的な型でのみ使用される場合、ジェネリック抽象化が本当に必要か、それとも具体的な実装の方がシンプルでコンパILが速くなる可能性があるかを検討してください。
 - 機能フラグ: Cargo の機能フラグを使用して、アプリケーションや依存関係の一部を有効/無効にし、特定のビルド構成(例:開発専用機能対本番環境機能)でコンパILされるコードの量を減らします。
 
結論
Rustのパフォーマンスと安全性への献身は、特に複雑なWebアプリケーションにおいて、ビルド時間の長期化というトレードオフを伴います。しかし、根本的な理由を理解し、cargo-watch のようなツールを戦略的に使用して自動再コンパIL、sccache によるビルドキャッシュ、および Cargo 設定の最適化を行うことで、開発者は貴重な開発時間を大幅に取り戻すことができます。これらの改善は、Rust Web開発体験を待機ゲームからスムーズで生産的なサイクルに変え、ビルド時間と戦うのではなく、堅牢でパフォーマンスの高いWebサービスを構築することに集中できるようになります。コンパILが一瞬で終わることはないかもしれませんが、これらのテクニックにより、驚くほど効率的にすることができます。