JavaScript開発者のためのRust入門:最初の1000行までの実践ガイド(2026年版)
「Rustは未来だ」という言葉、最近よく耳にしますよね。Stack Overflowでは8年連続で最も愛されている言語に選ばれています。DiscordはインフラをRustで再構築しました。Cloudflareはエッジサービスをこの言語で動かしています。Figmaのマルチプレイヤーエンジンも同様です。MicrosoftでさえWindowsのコアコンポーネントをRustで書き直しているほどです。
しかし問題があります。JavaScriptやTypeScript開発者からすると、ほとんどのRustチュートリアルはC++のバックグラウンドを前提にして書かれているように感じます。所有権?借用?ライフタイム?用語だけでタブを閉じて、慣れ親しんだnpm installに戻りたくなりますよね。
このガイドは違います。JavaScript開発者の視点でRustを学んでいきます。この記事を読み終える頃には、最初の1000行のRustコードを書いて、実際に何が起きているのかを理解できるようになります。
Web開発者がなぜRustを?
コードを書く前に、率直な話をしましょう:フロントエンド・バックエンド開発者が、なぜシステム言語を学ぶ必要があるのか?
パフォーマンスの現実
JavaScriptはインタプリタ(またはJITコンパイル)言語です。Rustはネイティブマシンコードにコンパイルされます。その差は微妙ではありません:
// JavaScript: JSONをパースして最大値を見つける const data = JSON.parse(hugeJsonString); const max = Math.max(...data.numbers); // 実行時間: 1000万個の数値で約450ms
// Rust: 同じ操作 let data: Data = serde_json::from_str(&huge_json_string)?; let max = data.numbers.iter().max(); // 実行時間: 1000万個の数値で約12ms
タイプミスではありません—同じロジックの操作で37倍高速です。これが重要になる場面があります:
- 瞬時に反応するCLIツールを作る時
- 大きなファイルを処理する時(ビルドツール、リンターなど)
- コールドスタート時間がコストになるサーバーレス関数を書く時
- ブラウザで重い計算を処理するWebAssemblyモジュールを作る時
WebAssemblyの架け橋
Web開発者にとって面白いのはここからです。Rustは他のどの言語よりもWebAssembly(WASM)にうまくコンパイルできます:
// このRustコードが... #[wasm_bindgen] pub fn fibonacci(n: u32) -> u32 { match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2) } }
...JavaScriptで直接インポートできる.wasmファイルになります:
import init, { fibonacci } from './pkg/my_rust_lib.js'; await init(); console.log(fibonacci(40)); // 純粋なJSより10-20倍速い
Figma、Photoshop(Web版)、Google Earthがまさにこのパターンをパフォーマンスクリティカルなコードに使用しています。
最初のRustプログラム:JavaScriptと比較する
馴染みのあるものから始めましょう。両方の言語でシンプルなプログラムを書いてみます:
JavaScript:
function greet(name) { const message = `Hello, ${name}!`; console.log(message); } greet("World");
Rust:
fn greet(name: &str) { let message = format!("Hello, {}!", name); println!("{}", message); } fn main() { greet("World"); }
すでにいくつかの違いが見えますね:
functionの代わりにfn- 型が明示的:
name: &str letは似た動作をするが、constは同じようには存在しないformat!とprintln!に!が付いているのはマクロだから- すべてのRustプログラムには
main関数が必要
環境をセットアップする
先に進む前に、Rustをインストールしましょう:
# Rustをインストール(macOS、Linux、WSLで動作) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # インストールを確認 rustc --version cargo --version
cargoはRustのパッケージマネージャであり、ビルドツールです—npmとwebpackを組み合わせたものだと考えてください。
# 新しいプロジェクトを作成(npm initのように) cargo new my-first-rust-app cd my-first-rust-app # プロジェクトを実行 cargo run
Rustの3大概念:オーナーシップ、ボローイング、ライフタイム
JS開発者がここでつまずくポイントです。でも大丈夫、JSの例えで説明していきますね。
問題:JavaScriptの隠れたメモリ管理
JavaScriptでは、メモリのことを考える必要がありません:
function processData() { const data = [1, 2, 3, 4, 5]; // メモリ割り当て const doubled = data.map(x => x * 2); // さらにメモリ割り当て return doubled; } // メモリは...いつかガベージコレクションされる // メモリがいつ解放されるかをコントロールできない // これがパフォーマンスクリティカルなコードで予期しないGCポーズを引き起こす可能性がある
JavaScriptはガベージコレクションを使用します。便利ですが予測不可能です。Rustは手動メモリ管理の危険なしにコントロールを与えてくれます。
所有権:常に一人のオーナー
Rustでは、すべての値には正確に一人の所有者がいます:
fn main() { let s1 = String::from("hello"); // s1が文字列を所有 let s2 = s1; // 所有権がs2に移動 // println!("{}", s1); // エラー!s1はもう何も所有していない println!("{}", s2); // 問題なし }
JavaScriptの用語で言えば、こんなことが起きると想像してください:
// JavaScriptに「所有権」があったら let s1 = "hello"; let s2 = s1; // Rustではこれがs1を無効化する console.log(s1); // Rustではこれがエラーになる!
なぜRustはこうするのでしょうか? s2がスコープを抜けるとき、Rustは正確にいつメモリを解放すべきかを知っているからです。ガベージコレクターは必要ありません。
借用:所有権なしの参照
でも待ってください—所有権を取らずに値を使うだけなら?それが借用です:
fn calculate_length(s: &String) -> usize { s.len() } // sはスコープを抜けるが、Stringを所有していないので何も起きない fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // &でs1を借用 println!("'{}'の長さは{}です。", s1, len); // s1はまだ有効! }
&は「見るだけで、取らないよ」と言っていると考えてください。
JavaScriptとの比較:
// JavaScriptはどちらにしてもオブジェクトを参照で渡す function calculateLength(s) { return s.length; } const s1 = "hello"; const len = calculateLength(s1); console.log(`'${s1}'の長さは${len}です。`); // 同じように動作
違いは何でしょう?Rustでは、コンパイラがcalculate_lengthがs1を変更したり保持したりできないことを保証します。JavaScriptでは、関数を信じるしかありません。
可変借用:単一ライタールール
Rustでは、以下のいずれかを持てます:
- 複数の不変参照(
&T) - または一つの可変参照(
&mut T)
同時に両方は持てません。
fn main() { let mut s = String::from("hello"); let r1 = &s; // OK:不変借用 let r2 = &s; // OK:もう一つの不変借用 // let r3 = &mut s; // エラー!不変借用中は可変借用できない println!("{} と {}", r1, r2); // r1とr2はこの時点以降使われない let r3 = &mut s; // これなら大丈夫! r3.push_str(", world"); }
これが重要な理由: このルールはコンパイル時にデータ競合を防ぎます。JavaScriptでは、コードの一部がオブジェクトを変更している間に別の部分が読み取ってバグが発生した経験があるのではないでしょうか。Rustはこれを不可能にします。
よく使うパターン:JS vs Rust
普段使っているコードをRustに置き換えてみましょう。
配列とイテレーション
JavaScript:
const numbers = [1, 2, 3, 4, 5]; // Map const doubled = numbers.map(x => x * 2); // Filter const evens = numbers.filter(x => x % 2 === 0); // Reduce const sum = numbers.reduce((acc, x) => acc + x, 0); // Find const firstEven = numbers.find(x => x % 2 === 0);
Rust:
let numbers = vec![1, 2, 3, 4, 5]; // Map let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); // Filter let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect(); // Reduce(Rustではfoldと呼ぶ) let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); // または単純に: let sum: i32 = numbers.iter().sum(); // Find let first_even = numbers.iter().find(|x| *x % 2 == 0);
主な違い:
|x|はRustのクロージャ構文(アロー関数のようなもの).iter()はイテレータを作成.collect()はイテレータをコレクションに戻す- 型を指定するか、Rustに推論させる必要がある
オプショナル値(nullの処理)
JavaScript:
function findUser(id) { const user = database.get(id); if (user === null || user === undefined) { return "Anonymous"; } return user.name; } // またはオプショナルチェイニングで const name = user?.name ?? "Anonymous";
Rust:
fn find_user(id: u32) -> String { let user = database.get(id); match user { Some(u) => u.name.clone(), None => String::from("Anonymous"), } } // より簡潔に let name = user.map(|u| u.name.clone()).unwrap_or(String::from("Anonymous")); // if letでさらに簡単に if let Some(user) = database.get(id) { println!("見つかりました: {}", user.name); }
Rustにはnullがありません。代わりにOption<T>を使います:
Some(value)= 値があるNone= 値がない
コンパイラが両方のケースを処理することを強制します。「undefined is not an object」エラーはもうありません!
エラー処理
JavaScript:
async function fetchData(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } catch (error) { console.error("Fetch失敗:", error); throw error; } }
Rust:
use reqwest; async fn fetch_data(url: &str) -> Result<serde_json::Value, reqwest::Error> { let response = reqwest::get(url).await?; let data = response.json().await?; Ok(data) } // 使用 match fetch_data("https://api.example.com/data").await { Ok(data) => println!("データ受信: {:?}", data), Err(e) => eprintln!("Fetch失敗: {}", e), }
?演算子はRustの「これが失敗したら、すぐにエラーを返す」に相当します。自動的なtry-catch伝播のようなものです。
実際に何か作ってみよう:JSON CLIツール
JS開発者が実際に使いそうなCLIツールを作ってみましょう—JSONフォーマッターです:
use std::env; use std::fs; use serde_json::{Value, to_string_pretty}; fn main() { // コマンドライン引数を取得(process.argvのように) let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("使い方: {} <file.json>", args[0]); std::process::exit(1); } let filename = &args[1]; // ファイルを読む(fs.readFileSyncのように) let contents = match fs::read_to_string(filename) { Ok(c) => c, Err(e) => { eprintln!("ファイル読み込みエラー: {}", e); std::process::exit(1); } }; // JSONをパース let parsed: Value = match serde_json::from_str(&contents) { Ok(v) => v, Err(e) => { eprintln!("不正なJSON: {}", e); std::process::exit(1); } }; // Pretty print match to_string_pretty(&parsed) { Ok(pretty) => println!("{}", pretty), Err(e) => eprintln!("フォーマットエラー: {}", e), }; }
実行するには:
# Cargo.tomlに依存関係を追加 # [dependencies] # serde_json = "1.0" cargo build --release ./target/release/json-formatter messy.json
コンパイルされたバイナリは約1MBでミリ秒で実行されます—CLIツールと一緒にNode.jsを配布するのと比べてみてください。
非同期Rust:それほど違わない
モダンJavaScriptはasync/awaitがすべてです。Rustにもあります:
JavaScript:
async function fetchMultiple(urls) { const promises = urls.map(url => fetch(url).then(r => r.json())); const results = await Promise.all(promises); return results; }
Rust(tokioランタイムを使用):
use futures::future::join_all; async fn fetch_multiple(urls: Vec<&str>) -> Vec<Result<String, reqwest::Error>> { let futures = urls.iter().map(|url| async move { let response = reqwest::get(*url).await?; response.text().await }); join_all(futures).await } #[tokio::main] async fn main() { let urls = vec![ "https://api.example.com/1", "https://api.example.com/2", ]; let results = fetch_multiple(urls).await; for result in results { match result { Ok(body) => println!("受信: {:.100}...", body), Err(e) => eprintln!("エラー: {}", e), } } }
構造は驚くほど似ています!主な違いは、Rustには明示的なasyncランタイムが必要なことです(tokioが最も人気)。
Web開発者のためのRustエコシステム
最もよく使うcrate(RustのnpmパッケージのFD)をご紹介します:
Webフレームワーク
- Axum - 新しいスタンダード、Tokioチームが構築
- Actix Web - 実戦テスト済み、極めて高速
- Rocket - 開発者フレンドリー、優れたエルゴノミクス
シリアライゼーション
- Serde - JSON、YAML、TOMLなどのデファクトスタンダード
- serde_json - JSON特化
HTTPクライアント
- Reqwest - RustのAxios
CLIツール
- Clap - 引数パース(commander.jsのようなもの)
- Indicatif - プログレスバー
- Colored - ターミナルの色
WebAssembly
- wasm-bindgen - JS/Rust相互運用
- wasm-pack - WASMパッケージのビルドと公開
パフォーマンス比較:実際の数字
現実的なワークロードを比較してみましょう—100MBのJSONファイルの処理:
| タスク | Node.js | Rust | 改善 |
|---|---|---|---|
| JSONパース | 2.3s | 0.18s | 12.7倍 |
| メール検索(正規表現) | 4.1s | 0.31s | 13.2倍 |
| 変換とシリアライズ | 3.8s | 0.24s | 15.8倍 |
| メモリ使用量 | 890MB | 210MB | 4.2倍少ない |
これらの数字が重要になる場面:
- ビルドツールを作る時(esbuildが同様の理由でGoで書かれている)
- ログや大規模データセットを処理する時
- メモリ制限のある環境で実行する時(サーバーレス、エッジ)
JS開発者がハマりやすいポイント
1. 文字列は複雑
let s1 = "hello"; // &str - 文字列スライス(借用) let s2 = String::from("hello"); // String - 所有された文字列 // これはできない: // let s3: String = "hello"; // エラー! // 変換が必要: let s3: String = "hello".to_string(); let s4: String = String::from("hello");
経験則:関数パラメータには&str、データを所有する必要がある時はStringを使う。
2. 例外なし、Resultのみ
// これはコンパイルできない - Resultを処理する必要がある let file = File::open("data.txt"); // Result<File, Error>を返す // 処理する必要がある let file = File::open("data.txt")?; // エラーを伝播 // または let file = File::open("data.txt").unwrap(); // エラーならパニック // または let file = File::open("data.txt").expect("ファイルを開けませんでした"); // メッセージ付きパニック
3. 不変がデフォルト
let x = 5; // x = 6; // エラー!変数はデフォルトで不変 let mut y = 5; y = 6; // OK!
これはJavaScriptのlet(可変)vs const(不変)の逆です。
4. 暗黙の型変換なし
let x: i32 = 5; let y: i64 = 10; // let z = x + y; // エラー!i32とi64を足せない let z = x as i64 + y; // 明示的に変換する必要がある
これからの学習プラン
JS開発者のための現実的なロードマップです:
1-2週目:基礎
- 「The Rust Book」の最初の8章を完了(オンラインで無料)
- 小さなプログラムを書く:FizzBuzz、ファイル読み込み、シンプルなCLI
3-4週目:所有権を深掘り
- 所有権の章を再読
- Rustlingsの演習を完了(インタラクティブな練習)
- ファイル永続化付きのTODO CLIアプリを作る
2ヶ月目:Web開発
- AxumでREST APIを構築
- データベースに接続(SQLxまたはDiesel)
- クラウドプラットフォームにデプロイ
3ヶ月目:WebAssembly
- WASMモジュールを構築
- React/Vue/Svelteアプリに統合
- 純粋なJavaScriptとパフォーマンスを比較
結論:で、Rustって学ぶ価値あるの?
JavaScript開発者にとって、Rustは代替ではなく補完です。WebアプリはTypeScriptで書き続けることになります。しかし、次のものが必要な時:
- 計算集約的なタスクに最大のパフォーマンス
- GCポーズのない予測可能なレイテンシ
- CLIツールやサーバーレス用の小さなバイナリ
- ブラウザパフォーマンス用のWebAssembly
...Rustが答えです。
学習曲線は本物です。所有権と借用は最初は混乱するでしょう。コンパイラは常にあなたのコードを拒否するでしょう(しかしエラーメッセージは本当に役立ちます)。
しかし一度クリックしたら—そして必ずクリックします—ほとんどのJavaScript開発者が持っていないスーパーパワーを手に入れることになります。メモリが実際にどのように機能するかを理解するようになります。どんな言語でもより安全なコードを書けるようになります。そしてJavaScriptでは単純に解決できない問題を解決できるツールを持つことになります。
小さなことから始めましょう。JSONフォーマッター。ファイルリネーマー。シンプルなCLIツール。コンパイラに教えてもらいましょう。そして気づいた時には、JavaScriptより20倍速く動くRustを書いているでしょう。
Rustへようこそ。コンパイラは厳格ですが、あなたの味方です。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう