「速い」は分かった。でも、なぜ速いのか
Rustがバックエンド開発で注目されている理由として、「C/C++並みの速度」「メモリ安全性」という言葉がよく出てくる。しかしその言葉だけでは、何かを作るときの判断には使えない。「速い」の裏側にある仕組みを知っておくことで、初めて「どこで使うべきか」の判断ができるようになる。
Rustが速い本質的な理由は、ガベージコレクタ(GC)を持っていないことにある。
JavaやGoやNode.jsはGCという仕組みを持っていて、使われなくなったメモリを定期的に自動で解放してくれる。これは開発者にとってはありがたいが、GCが動くタイミングで処理が一時的に止まる——これが「予測不可能な遅延(GCポーズ)」として現れる。
Rustは代わりに「所有権(Ownership)」という概念をコンパイラに組み込んでいる。変数がいつ作られ、いつ誰かに渡され、いつ不要になるかを、プログラマが明示的に決め、コンパイラがそれを静的に検証する。つまり、メモリの解放はコンパイル時に決まっていて、実行中に止まることがない。
これはGCがある言語との本質的な違いだ。Node.jsで書いたAPIサーバーが、特定の条件下で突然レスポンスが遅くなる現象に悩まされた経験があるなら、その原因の多くはGCポーズだ。Rustにはその問題が構造的に存在しない。
所有権は「制約」ではなく「仕様書」だ
Rustを学び始めたとき、最初に多くの人がつまずくのが所有権とライフタイムという概念だ。コンパイルが通らない、意味不明なエラーが出る、という「コンパイラとの戦い」は、Rust入門者の通過儀礼としてよく語られる。
しかし別の見方をすると、コンパイラが怒るのは「このコードは実行時に壊れる」という予測を、事前に教えてくれているということだ。
fnmain(){lets1=String::from("hello");lets2=s1;// s1の所有権がs2に移動println!("{}",s1);// ←コンパイルエラー:s1はもう使えない}
このコードは他の言語なら動く。しかしRustはここでエラーを出す。s1の所有権がs2に「移動(ムーブ)」したため、s1はもう無効だ——これがRustの所有権ルールだ。
他の言語でこれが問題にならないのは、GCが後から「誰も使っていない」と判断して掃除してくれるからだ。Rustはその判断をコンパイル時に終わらせる。本番環境でメモリが二重解放されてクラッシュする、というクラスのバグは、Rustでは原理的に起きない。
コンパイルが通れば、本番でも落ちないという信頼感は、この所有権システムから来ている。
3つのWebフレームワーク:何が違うのか
Rustのバックエンドフレームワークは主に3つある。それぞれ「速さより書きやすさ」「書きやすさより速さ」というトレードオフの設計思想が違う。
Axum:今から始めるならこれ
useaxum::{routing::get,Router};asyncfnhello()->&'staticstr{"Hello, CONSTRUCT LAB"}#[tokio::main]asyncfnmain(){letapp=Router::new().route("/",get(hello));letlistener=tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();axum::serve(listener,app).await.unwrap();}
非同期ランタイム「Tokio」を開発しているチームが作ったフレームワーク。RustのWebエコシステムは「Tokioの上に何を積むか」という構造になっているため、Tokioチームが作ったAxumは、そのエコシステム全体と最も自然に組み合わさる。ドキュメントが整っていて、新しいライブラリのサポートも最初にAxumに来ることが多い。迷ったらAxumを選んでいい。
Actix-web:ベンチマークの王者
ベンチマークサイトで常にトップクラスの数値を出し続けているフレームワーク。実績もドキュメントも豊富で、本番投入の実例も多い。ただし、Axumより独自の概念が多く、学習コストはやや高い。「とにかく数字で速さを出したい」という要件がある場合の選択肢になる。
Rocket:「書いていて気持ちいい」を優先する
#[get("/hello/<name>")]fnhello(name:&str)->String{format!("Hello, {}!",name)}
マクロ(アノテーション)で直感的に書けるように設計されている。PythonのFlaskやRubyのSinatraに近い感覚で書けるため、他の言語から来た開発者が最初に動くものを作るには向いている。速さよりも「Rustの学習曲線を下げながら動くものを作る」ことを優先した設計だ。
データベース層:型安全の恩恵がここでも出る
RustのDB連携ライブラリで特徴的なのがSQLxのコンパイル時SQL検証だ。
letrow=sqlx::query!("SELECT id, name FROM ships WHERE id = $1",ship_id).fetch_one(&pool).await?;
query!マクロを使うと、ビルド時に実際のデータベースに接続して、SQLの構文が正しいか・カラム名が存在するかを検証する。タイポや存在しないカラム名を指定したコードは、コンパイルが通らない。本番で「カラム名を間違えていた」というミスが原理的に起きなくなる。
他の言語のORMでは、クエリの誤りは実行時まで分からないことが多い。Rustの型システムとコンパイル検証の組み合わせは、バックエンドの堅牢性を「仕組みとして」担保する。
ORMの選択肢としては、モダンな非同期ファーストのSeaORMと、歴史のあるDieselがある。SeaORMはSQLxの上に構築されていて動的クエリに強い。DieselはRustでの実績が最も長く型安全性が高いが、非同期対応は後から追加されたため、Axumと組み合わせる際には設定が少し複雑になる。
WebAssembly:「フロントエンドもRustで」という選択肢
RustはブラウザのフロントエンドにもWebAssembly(WASM)経由で使える。LeptosやYewというフレームワークを使うと、JavaScriptを書かずにRustだけでUIを構築できる。
// Leptosでの記述例(Reactに近い)#[component]fnShipCard(ship_name:String)->implIntoView{view!{<div class="card"><h2>{ship_name}</h2></div>}}
これが向いているのは、ブラウザ内で計算量の多い処理を行うアプリだ。画像処理、物理シミュレーション、大量データのリアルタイムフィルタリングなど、JavaScriptではCPUの限界に当たる処理をネイティブに近い速度で動かせる。
一方で「とりあえずUIを作りたい」という用途には向いていない。ReactやVueの資産(ライブラリ、チュートリアル、Stack Overflowの回答)の圧倒的な量に対して、LeptosやYewのエコシステムはまだ小さい。「Rustを使いたい」という動機より「この処理をブラウザで速く動かしたい」という具体的な理由がある場合に検討する技術だ。
正直なデメリット:「向いていない場所」を知る
Rustのデメリットも、仕組みから理解すると判断しやすくなる。
学習コストの高さは、所有権とライフタイムという概念の習得コストだ。他の言語では自由にできたメモリの扱いに制約が加わる分、コンパイラと格闘する時間がかかる。「コードを書き始めてすぐ動くものを作りたい」というプロトタイピングの初速は、Node.jsやPythonより遅い。
コンパイル時間は、Rustが安全性チェックをコンパイル時に行う代償だ。プロジェクトが大きくなるにつれて、変更を加えるたびのビルド時間が気になり始める。開発サイクルの高速な反復が重要な初期フェーズでは、これが足かせになることがある。
エコシステムの網羅性は、まだJavaScriptやPythonには及ばない。特定のSaaSのSDK、認証ライブラリの選択肢、ニッチなAPIのクライアント——これらを探すと「RustのCrateがまだない」という場面に出くわす。そのたびに自作するか、C/C++のライブラリをFFIで呼び出すかという判断が必要になる。
どこで使うか:「全部Rustで」ではなく「重い部分だけ」
前の記事でも書いたが、既存のサービスにRustを導入するなら「全部を置き換える」発想より「重い処理だけを置き換える」発想が現実的だ。
- 既存のWordPressサイトに、特定の処理だけWebAssembly化して組み込む
- Node.jsで書いたAPIサーバーのうち、CPUを使うエンドポイントだけをRustのマイクロサービスとして切り出す
- ビルドツールやCLIツールをRustで書いて、速度の恩恵を受ける
「Rustを使う」ことを目的にするよりも、「このボトルネックをなくしたい」という具体的な問題から逆算してRustに辿り着く、という順序の方が、投資対効果として合いやすい。
所有権という制約を理解し、コンパイラと和解できたとき、バグの少なさとパフォーマンスの予測可能性を同時に手に入れられる——それがRustをWeb開発に持ち込む動機として一番誠実な言い方だと思う。