Rust Webアプリのビルドを高速化する
Wenhao Wang
Dev Intern · Leapcell

Rust Web開発におけるビルドオーバーヘッドの課題
Rustはそのパフォーマンスとメモリ安全性から、Web開発においてますます魅力的な選択肢となっています。Axum、Actix-Web、Rocketのようなフレームワークも人気を集めています。しかし、開発者体験、特にコンパイル時間に関しては、ボトルネックになりがちです。インタプリタ言語やガベージコレクション付きのコンパイル言語とは異なり、Rustの強力な型システム、借用チェッカー、洗練されたオプティマイザは、「cargo build」に時間がかかることを意味し、特に大規模なWebアプリケーションでは、その遅さが開発の「インナー開発ループ(IDL)」を妨げ、生産性と創造性にとって不可欠な迅速なフィードバックを阻害することがあります。
わずかな変更で数分待たされるコンパイルへのフラストレーションは、Rustでの開発の楽しさをすぐに損なう可能性があります。最初の完全なビルドは許容できても、Rustの優れたキャッシュメカニズムがあっても、その後のインクリメンタルビルドは遅延の原因となり得ます。この記事では、Rust製Webアプリケーションのコンパイルが遅くなる根本的な理由を掘り下げ、さらに重要な点として、sccache、cargo-watch、最新のリンカ(lld/mold)のようなツールを使用して、ビルド時間を劇的に改善し、開発の俊敏性を取り戻すための実践的で実行可能な戦略を提供します。
コンパイルの全体像を理解する
最適化に入る前に、Rustのコンパイル速度に影響を与える主要な概念について共通の理解を確立しましょう。
- コンパイル単位: Rustでは、「クレート」(メインアプリケーションまたはそれに依存するライブラリ)が主要なコンパイル単位です。コンパイラは各クレートを個別に処理します。
- インクリメンタルコンパイル: Rustのコンパイラはインテリジェントに設計されています。小さな変更を加えると、以前のビルドからのキャッシュされた成果物を活用し、影響を受けたコードの部分のみを再コンパイルしようとします。しかし、たとえ「小さい」変更であっても、キャッシュの大部分を無効にし、大幅な再コンパイルを引き起こすことがあります。
- 依存関係グラフ: Webアプリケーションは通常、多数のサードパーティ製クレート(Webフレームワーク、シリアライゼーションライブラリ、非同期ランタイムなど)に依存しています。これらの依存関係はそれぞれコンパイルされる必要があり、特に最初のビルドでは、そのコンパイルがビルド時間全体のかなりの部分を占める可能性があります。直接の依存関係の変更は、コードの再コンパイルを引き起こす可能性があります。
- リンカ: すべてのオブジェクトファイル(
.oファイル)がコンパイルされた後、リンカはそれらを結合して実行可能ファイルを生成する役割を担います。このプロセスは、多数のシンボルを持つ大規模なアプリケーションでは、リンカがすべての相互参照を解決する必要があるため、驚くほど時間がかかることがあります。 - デバッグビルド vs. リリースビルド: デバッグビルド(
cargo build)は高速なコンパイルを優先し、デバッグ情報を含みますが、実行時パフォーマンスは犠牲になります。リリースビルド(cargo build --release)は広範な最適化を実行し、コンパイルは遅くなりますが、バイナリは高速で小さくなります。ローカル開発では、ほぼ常にデバッグビルドを使用します。
コンパイルが遅い根本的な原因は、Rustの安全性とパフォーマンスの保証にあります。コンパイラは、借用チェック、型チェック、最適化パスなど、計算負荷の高い広範な分析を実行します。Webアプリケーションは本質的に多くの依存関係を引き込むことが多く、処理されるべき深く広い依存関係グラフを作成します。
Rust Webアプリのビルドをスーパーチャージする戦略
コンパイル時間の遅さに立ち向かうためのツールとテクニックを探りましょう。
1. sccache による外部キャッシュ
cargoには組み込みのインクリメンタルコンパイルがありますが、sccacheは、すべてのRustプロジェクト(およびC/C++プロジェクトも!)の共有グローバルキャッシュを提供することで、キャッシュを次のレベルに引き上げます。コンパイラ呼び出しをインターセプトし、ファイルが変更されておらず、入力が同じであれば、キャッシュされた出力を直接提供します。これは、めったに変わらない大規模な依存関係ツリーに特に効果的です。
インストール:
cargo install sccache --locked
設定:
cargoにsccacheを使用させるには、環境変数を設定する必要があります。一貫して使用するための最も簡単な方法は、シェル設定(.bashrc、.zshrcなど)またはプロジェクト固有の.cargo/config.tomlに追加することです。
オプション1:環境変数(例:~/.bashrcまたは~/.zshrc)
export RUSTC_WRAPPER="sccache" export SCCACHE_DIR="$HOME/.sccache" # オプション:キャッシュディレクトリを指定 export SCCACHE_CACHE_SIZE="10G" # オプション:キャッシュサイズを指定
その後、シェルをリロードするか、新しいターミナルを開きます。
オプション2:プロジェクト固有の.cargo/config.toml(sccacheが不可欠なプロジェクトに推奨)
プロジェクトのルートにある.cargo/config.tomlを作成または編集します。
# .cargo/config.toml [build] rustc-wrapper = "sccache"
sccacheが動作していることを確認する:
セットアップ後、sccache --show-statsを実行してキャッシュアクティビティを確認します。
$ sccache --show-stats Compile stats for sccache version 0.7.3-alpha.0 (bbceb34b 2023-08-04): ... Compile requests 42 Cache hits 30 Cache misses 12 Cache hit rate 71.43% ...
特に最初の完全ビルド後、sccacheが変更されていない依存関係のキャッシュをヒットするため、大幅な速度向上が見られるでしょう。
2. cargo-watch による自動再コンパイルと再起動
INNER開発ループは、即座のフィードバックを得ることすべてです。変更ごとに手動でcargo buildまたはcargo runを実行することは、すぐに面倒になります。cargo-watchは、ソースファイルを変更がないか監視し、変更が検出されたときにコマンドを自動的に再実行することで、このプロセスを自動化します。
インストール:
cargo install cargo-watch --locked
Webアプリケーションでの使用:
通常、コードが変更されたときにWebサーバーを再コンパイルして再起動したいでしょう。
cargo watch -x run
これを分解しましょう。
cargo watch: メインコマンド。-x run: ファイルが変更されるたびにcargo runを実行します。
Webアプリケーションでは、変更されていない依存関係の完全な再コンパイルを回避することも役立つかもしれません。cargoのインクリメンタルコンパイルはこれをうまく処理しますが、sccacheとcargo-watchを組み合わせて使用することで、最大の効率が保証されます。
シンプルなAxumアプリでの例:
src/main.rs:
use axum::[ routing::get, Router, ]; #[tokio::main] async fn main() { // 単一のルートでアプリケーションを構築する let app = Router::new().route("/", get(handler)); // `hyper` で `localhost:3000` で実行する let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn handler() -> &'static str { "Hello, Axum World!" }
そしてcargo watch -x runを実行します。src/main.rsを保存する(例:「Hello, Axum World!」を「Hi, Axum!」に変更する)と、cargo-watchは変更を検出し、sccacheは(他の依存関係がキャッシュされている場合)src/main.rsファイルのみが再コンパイルされるようにし、サーバーはほぼ瞬時に再起動します。
必要に応じて、-w(watch)および-i(ignore)フラグを使用して、監視または無視するフォルダを指定することもできますが、デフォルトは通常うまく機能します。
3. lld または mold による高速リンカ
コンパイル後、リンカは実行可能ファイルを生成する最終ステップを実行します。大規模なRustアプリケーションでは、リンクがビルド時間のかなりの部分を占めることがあります。デフォルトのldリンカは遅い可能性がありますが、lld(LLVMのリンカ)やmoldのような最新の代替手段は劇的な速度向上を提供します。
lld (LLVMリンカ)
lldはLLVMプロジェクトの高パフォーマンスリンカです。多くの場合、システムパッケージマネージャで利用可能です。
インストール(Linux - 多くの場合プリインストールされているか、llvmをインストール):
# Ubuntu/Debian sudo apt install lld # Fedora/RHEL sudo dnf install lld
インストール(macOS - Homebrew経由):
brew install llvm # これは通常、llvmパッケージの一部としてlldをインストールします
設定:
.cargo/config.tomlでcargoをlldを使用するように設定できます。
# .cargo/config.toml [target.x86_64-unknown-linux-gnu] # OSに合わせてターゲットトリプルを調整 linker = "clang" # Linuxではシステム設定に応じて "gcc" rustflags = ["-C", "link-arg=-fuse-ld=lld"] [target.aarch64-apple-darwin] # macOS Apple Silicon 用 linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=lld"] [target.x86_64-apple-darwin] # macOS Intel 用 linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=lld"]
linker = "clang"(またはgcc)は、Rustにそれをドライバーとして使用するように指示し、link-arg=-fuse-ld=lldは、そのドライバーに実際のリンクにlldを使用するように指示します。x86_64-unknown-linux-gnuを自身のターゲットトリプル(例:Apple Silicon Macの場合はaarch64-apple-darwin)に置き換えてください。ターゲットトリプルはrustc --print target-tripleで確認できます。
mold
moldは、Rui Ueyama(lldの作成者)によって開発された、さらに新しい、非常に高速なリンカです。ldとlldの両方よりも大幅に高速になるように設計されています。
インストール(Linux):
# 推奨:GitHubリリースのプリビルドバイナリをダウンロード # 例:x86-64 Linux の場合: wget https://github.com/rui314/mold/releases/download/v2.3.0/mold-2.3.0-x86_64-linux.tar.gz tar -xf mold-*.tar.gz sudo cp mold-*/bin/mold /usr/local/bin/mold sudo cp mold-*/lib/mold /usr/local/lib/mold # 一部の設定で必要になる場合があります
LD_LIBRARY_PATHを調整するか、moldをシステム全体にインストールする必要がある場合があります。
インストール(macOS - Homebrew経由):
brew install mold
moldの設定:
lldと同様に、.cargo/config.tomlを介してcargoを設定します。
# .cargo/config.toml [target.x86_64-unknown-linux-gnu] rustflags = ["-C", "link-arg=-fuse-ld=mold"] # Linux linker = "clang" # または "gcc" [target.aarch64-apple-darwin] # macOS Apple Silicon rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "clang" [target.x86_64-apple-darwin] # macOS Intel rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "clang"
設定後、cargo build出力の「Linking」フェーズで即座に、しばしば半分以上に短縮されるのを確認できるでしょう。
組み合わせワークフロー
最も効果的なアプローチは、これらのツールをすべて組み合わせることです。
- グローバル
sccacheセットアップ: シェル環境でRUSTC_WRAPPER="sccache"が設定されていることを確認するか、プロジェクト固有の.cargo/config.tomlを割り当てます。 lldまたはmoldの統合: プロジェクトの.cargo/config.tomlにリンカ設定を追加します。- ローカル開発ワークフロー:
cargo watch -x runを使用して、sccacheと高速リンカと連携した自動再コンパイルと再起動の利点を活用します。
さらに高速な「監視と再実行」サイクル、特に小さな変更中に、デバッグ情報の一部をCargo.tomlから省略することを検討することもできます。
# Cargo.toml [profile.dev] # デフォルトは2(完全なデバッグ情報)、0は削除します。1は「行テーブルのみ」です。 # より高速なコンパイルのために0または1を使用しますが、適切なデバッグのためには元に戻すことを忘れないでください。 debug = 0
注意: debug = 0 を設定すると、デバッグ可能性が大幅に低下します(例:ブレークポイントが機能しません)。デバッグなしで最も高速なビルド時間が必要な場合にのみ、バグ関連以外の変更を反復処理している場合に使用してください。値1はしばしば良いバランスを提供します。
結論
Rust Web開発における遅いコンパイル時間は、言語固有の安全性とパフォーマンスの複雑さ、そして最新Webアプリケーションの深い依存関係グラフにより、生産性にとって大きな障害となり得ます。しかし、インテリジェントなビルドキャッシュのためにsccache、自動再コンパイルと再起動のためにcargo-watch、そしてリンクオーバーヘッドを劇的に削減するためのlldまたはmoldのような先進的なリンカを戦略的に活用することで、Rust開発体験をフラストレーションの待機から流動的な反復へと変えることができます。これらのツールを採用することは、貴重な時間を節約するだけでなく、最終的にはRustで堅牢でパフォーマンスの高いWebサービスを構築する楽しさを取り戻します。

