Leptos(レプトス)は、画面もサーバー処理もRustで書けるフルスタックのWebフレームワークです。ブラウザ側のコードはWebAssembly(WASM)にコンパイルされ、仮想DOMを使わずにシグナルで変わった箇所だけを書き換えます。この記事では、2026年10月17日時点の安定版 0.8.22 で実際にビルドした結果をもとに、Trunkで動かすCSRの最小構成、cargo-leptosとAxumで組むSSR、server関数とResource、Yew・Dioxusとの違い、そして公開直後の 0.9.0-rc0 で変わる点までを説明します。
まとめ:Leptosの使い方と選び方の要点
- Leptosの安定版は0.8.22(2026-10-05公開)、次の0.9系はリリース候補0.9.0-rc0(2026-10-15公開)の段階です。ライセンスはMITです。
- ブラウザだけで動かすなら
csrフィーチャとTrunk、サーバーで初回HTMLを返すなら cargo-leptos と公式テンプレート start-axum を使います。 - Intel Mac で0.8.22のカウンターをリリースビルドすると、初回のコンパイルに4分27秒かかり、出力されたWASMは約165KB(gzipで約43KB)でした。
- 状態は
signal()・Memo::new・Effect::newで持ちます。0.8系のstable Rustでは読み出しに.get()が必要で、count()と書けるのは0.9からです。 - YewとDioxusは仮想DOM方式、LeptosとSycamoreは細粒度リアクティブ方式です。デスクトップやモバイルまで1つのコードで出したいならDioxus、Webのフルスタックに絞るならLeptosが合います。
Leptosの更新方式:シグナルによるDOMの部分更新
Leptosのコンポーネントは、#[component] を付けた関数が view! マクロでHTMLを返す形で書きます。ReactやYewのように状態が変わるたびに関数全体を呼び直して差分を取るのではなく、コンポーネント関数は最初に1回だけ実行されます。その後は、シグナルを読んでいるテキストや属性だけが直接書き換わります。SolidJSと同じ考え方で、公式サイトも「細粒度のリアクティブシグナルが、状態の変化に応じてDOMを狙って更新する」と説明しています。JavaScript側での同じ仕組みはSolidJSとは?Reactとの違い・Signalで速い理由・導入手順【2026年版】で扱っています。
signal・Memo・Effectの書き方
状態を扱う部品は3つです。signal(初期値) が読み取り用と書き込み用の組を返し、Memo::new は他のシグナルから計算した値をキャッシュし、Effect::new は値が変わったときに副作用(ログ出力やlocalStorageへの保存など)を実行します。次のコードは、後の章でTrunkを使って実際にビルドしたものと同じです。
use leptos::prelude::*;
#[component]
fn Counter(initial: i32) -> impl IntoView {
let (count, set_count) = signal(initial);
let doubled = Memo::new(move |_| count.get() * 2);
Effect::new(move |_| {
leptos::logging::log!("count = {}", count.get());
});
view! {
<button on:click=move |_| *set_count.write() += 1>"+1"</button>
<p>"count: " {count} " / doubled: " {doubled}</p>
}
}
fn main() {
console_error_panic_hook::set_once();
mount_to_body(|| view! { <Counter initial=0 /> });
}
{count} のようにシグナルをそのままビューに置くと、その位置のテキストノードだけが更新対象として登録されます。{count.get()} と書くと、取り出した値が1回埋め込まれるだけで、その後は更新されません。最初につまずきやすいのはこの違いです。
Leptos 0.8系とstable Rustでのsignal呼び出し制約
Leptos Bookには count() と書く例も出てきますが、これは0.8系ではnightlyのコンパイラと nightly フィーチャを使ったときの書き方です。stable Rust 1.98.0 と leptos 0.8.22 で count() * 2 と書くと、error[E0618]: expected function, found `leptos::prelude::ReadSignal<i32>` でコンパイルが止まりました。0.8系をstableで使う間は .get() と .set()(または .write())で書きます。0.8.22の Cargo.toml で宣言されている最小Rustバージョンは1.88です。
TrunkでのLeptos CSRアプリの作成と起動
サーバーを持たず、静的ホスティングに置けるSPAとして動かす構成です。Leptos Bookの導入手順どおり、WASMのビルドターゲットとTrunkを入れ、csr フィーチャでLeptosを追加します。Rust本体とcargoをまだ入れていない場合は、Rust入門|環境構築から所有権・基本文法、受託開発での採用判断までとCargo(Rust)とは?インストールと使い方・主要コマンド一覧を先に見てください。
rustup target add wasm32-unknown-unknown
cargo install trunk --locked
cargo new leptos-counter
cd leptos-counter
cargo add leptos@=0.8.22 --features=csr
cargo add console_error_panic_hook
プロジェクト直下に、headに文字コードとタイトルだけを書き、bodyは空にした index.html を置きます。Trunkはこのファイルを起点にビルドし、WASMとそれを読み込むJavaScriptを差し込みます。
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Leptos Counter</title></head>
<body></body>
</html>
src/main.rs に前章のカウンターを書き、trunk serve --open を実行すると、ファイルを保存するたびに再ビルドされる開発サーバーが起動してブラウザが開きます。本番用の成果物は trunk build --release で dist/ に出力されます。
CSRのビルド時間とWASMサイズの実測
Intel Core i9-9880H の Mac(rustc 1.98.0、Trunk 0.21.14)で、上のカウンターを Cargo.toml の [profile.release] に opt-level = "z"・lto = true・codegen-units = 1 を指定してリリースビルドしました。依存クレートのキャッシュが無い初回の状態です。
| 項目 | 結果 |
|---|---|
| trunk build –release(初回) | 4分40秒(cargo部分は4分27秒) |
| WASM(_bg.wasm) | 168,914バイト |
| WASMをgzip -9で圧縮 | 43,931バイト |
| 読み込み用JavaScript | 25,088バイト |
| wasm-bindgen | 0.2.129をTrunkが自動取得 |
この数値は wasm-opt を通していないものです。今回の index.html には data-trunk の指定を書いておらず、ビルドログにも wasm-opt の取得や実行は出ませんでした。サイズを詰めるときは <link data-trunk rel="rust" data-wasm-opt="z" /> のように最適化レベルを指定します(Trunkのアセット設定。--release のときだけ有効)。ボタン1つのアプリでも初回のコンパイルに4分以上かかる点は、CIのキャッシュ設計に直接響きます。
cargo-leptosとAxumでのLeptos SSRアプリの作成
初回表示をサーバーで描画し、ブラウザ側でWASMを読み込んで操作できる状態にする(ハイドレーション)構成です。サーバーとクライアントを別々のフィーチャで2回ビルドする必要があるため、専用のビルドツール cargo-leptos(2026年10月17日時点の最新は0.3.11)を使います。Axum用の公式テンプレートは start-axum、Actix Web用は start-actix です。
cargo install cargo-leptos --locked
cargo leptos new --git https://github.com/leptos-rs/start-axum
# 生成時にプロジェクト名を my-app と入力した場合
cd my-app
cargo leptos watch
生成される Cargo.toml には hydrate と ssr の2つのフィーチャがあります。ssr がaxum・tokio・leptos_axumを、hydrate がWASM側の依存を有効にします。[package.metadata.leptos] の site-addr = "127.0.0.1:3000" が開発サーバーのアドレスで、画面の編集は src/app.rs から始めます。テンプレートのREADMEは、cargo-leptosが既定でnightly Rust・cargo-generate・sassを使うとしており、ビルドで止まったらこの3つを入れるよう案内しています。Axum自体の書き方はAxum(アクサム)とは?Rust製Webフレームワークの使い方とActix Web比較【axum 0.8対応】を参照してください。
SSRが出力するHTMLの中身
前章と同じカウンターを ssr フィーチャでビルドし、to_html() で文字列にすると、次のHTMLが出力されました(初期値3)。
<button>+1</button><p>count: <!>3<!> / doubled: <!>6</p>
<!> は、シグナルの値が入るテキストの境界をハイドレーション時に見つけるための空コメントです。on:click の処理はHTMLに含まれず、WASMが読み込まれた後に取り付けられます。そのため、WASMの読み込みが終わるまでボタンは押しても反応しません。検索エンジンには値の入ったHTMLが渡るので、CSRよりSEOに有利です。
server関数とResource・Suspenseでのデータ取得
#[server] を付けたasync関数は、ssr ビルドではサーバー上で本体が実行され、csr や hydrate のビルドから呼ぶとサーバーへのHTTPリクエストに置き換わります(Leptos Bookのserver関数の章)。APIのエンドポイントと、それを呼ぶfetchのコードを別々に書かずに済みます。読み込み待ちは Resource と Suspense で扱います。次のコードは leptos 0.8.22 の ssr フィーチャでコンパイルが通ることを確認しました。
use leptos::prelude::*;
#[server]
pub async fn get_user_name(id: u32) -> Result<String, ServerFnError> {
// ssr ビルドではこの本体がサーバー上で実行される
Ok(format!("user-{id}"))
}
#[component]
fn UserCard() -> impl IntoView {
let (id, set_id) = signal(1u32);
let name = Resource::new(move || id.get(), |id| get_user_name(id));
view! {
<button on:click=move |_| set_id.update(|n| *n += 1)>"次のユーザー"</button>
<Suspense fallback=move || view! { <p>"読み込み中..."</p> }>
{move || name.get().map(|r| match r {
Ok(n) => view! { <p>{n}</p> }.into_any(),
Err(e) => view! { <p>"エラー: " {e.to_string()}</p> }.into_any(),
})}
</Suspense>
}
}
Resource::new の第1引数がシグナルを読むので、ボタンで id が変わると get_user_name が呼び直されます。読み込み中は fallback が表示されます。分岐ごとにビューの型が違うため、.into_any() で型をそろえています。なお、このコンポーネントを main から単体で to_html() しようとすると、「グローバルなexecutorが初期化される前にFutureをspawnしようとした」というpanicで止まりました。原因は非同期処理を動かすexecutorの未初期化です。アプリではleptos_axumなどのサーバー統合が初期化を担い、単体で試すときは any_spawnerのExecutor にある init_tokio などで先に初期化します。
Tailwind CSSの設定と周辺クレートの対応版確認
cargo-leptos 0.3.11 は、Cargo.toml の [package.metadata.leptos] に tailwind-input-file で入力CSSのパスを書くとTailwindのビルドを有効にします。設定ファイルは tailwind-config-file で指定でき、使うTailwindの版は環境変数 LEPTOS_TAILWIND_VERSION で変えられます(cargo-leptosのREADME)。Node.jsでTailwindを別に動かす必要はありません。ブラウザAPIのラッパーをまとめた leptos-use は0.19.3が leptos ^0.8 に依存しています。UIコンポーネントのクレートを選ぶときは、crates.ioの依存欄で対応するLeptosの版を確認してから入れてください。Leptosは0.6・0.7・0.8と版ごとにAPIが変わってきたため、更新が止まったクレートは最新のLeptosと組み合わせられないことがあります。
Yew・Dioxus・Sycamoreとの違い
RustでWebのUIを書くフレームワークの主な4つを、2026年10月17日時点の最新版で並べます。スター数は各GitHubリポジトリの2026年10月17日時点の値を概数にしたものです。
| 項目 | Leptos | Dioxus | Yew | Sycamore |
|---|---|---|---|---|
| 最新版 | 0.8.22 | 0.7.10 | 0.23.0 | 0.9.4 |
| 更新方式 | 細粒度リアクティブ | 仮想DOM | 仮想DOM | 細粒度リアクティブ |
| SSR | 対応(ssr・hydrate) | 対応(fullstack) | 対応(ssr・hydration) | hydrateフィーチャあり |
| server関数 | あり | あり | なし | なし |
| デスクトップ・モバイル | Tauri等と併用 | 本体で対応 | Tauri等と併用 | Tauri等と併用 |
| GitHubスター | 約2.1万 | 約3.9万 | 約3.3万 | 約3,400 |
Dioxusは、Web・デスクトップ・モバイルを1つのコードから出すことを掲げ、server関数とTailwindの組み込み対応も持ちます。描画は VirtualDom で差分を計算する方式です。YewはReactに近い書き味の仮想DOM方式で、最も古くから使われてきました。0.23.0は2026年3月の公開から版が上がっていません。SycamoreはLeptosと同じ細粒度リアクティブ方式ですが、GitHubスター数は比較した4つの中で最少です。ただし、スター数は関心の目安であり、実際の利用者数ではありません。
選び方は次のとおりです。ネイティブのデスクトップアプリやモバイルアプリまで同じコードで作るならDioxusを選びます。Webアプリをサーバー込みでRustに寄せ、SSRとserver関数を中心に組むならLeptosを選びます。Leptosでデスクトップアプリを作る場合は、フロントエンドをLeptosで書いてTauriとは?Electronとの違い・将来性・始め方まで徹底解説【2026年版】で包む構成になります。Yewは、既にYewで書いたコードがある場合や、チームがReactの書き方に慣れていてSSRとserver関数を重視しない場合の候補になります。
Leptos 0.9で変わる点と0.8からの移行
0.9.0-rc0は2026年10月15日に公開されました。リリースノートは「ほとんどのアプリは0.8から0.9へ上げるだけで変更が要らない」としたうえで、次の変更を挙げています。
| 変更 | 0.8系 | 0.9系 |
|---|---|---|
| signalの読み出し | stableは .get() | stableでも count() |
| 遅延読み込み・コード分割 | 標準で含む | lazyフィーチャが必要 |
| server関数の入力エンコード | serde_qs 0.x | serde_qs 1.0 |
| on:toggleのイベント型 | Event | ToggleEvent |
| Signal<T>をimpl Fn() -> Tに渡す | foo=signal | foo=*signal が必要な場合あり |
signalを関数として呼ぶ書き方は、0.9.0-rc0でstable Rust 1.98.0のまま set_count(initial + 1) と count() * 2 を書き、SSRの出力が count: <!>4<!> / doubled: <!>8 になることを確かめました。同じコードは0.8.22では先に示したE0618でコンパイルできません。つまり、0.9の書き方で書いたコードは0.8へ戻せません。
移行の判断ははっきりしています。本番で動かしているアプリは0.8.22のまま正式版0.9.0を待ちます。RCの段階では周辺クレート(leptos-useやUIライブラリ)がまだ0.8系にしか対応していないことが多いからです。これから新しく書き始めるプロジェクトは0.8.22で始め、.get() で書いておけば0.9でもそのまま通ります。遅延読み込み(#[lazy] やルートのコード分割)を使っているアプリは、0.9に上げるときに lazy フィーチャを足すのを忘れないでください。
Leptosを採用すべきでない場面
次の条件に当てはまるなら、Leptosよりも React・Vue・SolidJS などのJavaScriptフレームワークを選んだほうが早く終わります。
- 画面の修正を短いサイクルで回すチームにRust経験者が少ない:ボタン1つのアプリでも、依存クレートを含む初回のリリースビルドに4分以上かかりました。日常の編集で効くのは増分ビルドの時間ですが、CIで毎回キャッシュなしにビルドする構成ではこの初回の時間がそのまま待ち時間になります。所有権とライフタイムの制約はUIのコードにも及び、クロージャに渡す値の
clone()やmoveが頻繁に出てきます。 - 既製のUIコンポーネントを多く使いたい:日付ピッカー、データグリッド、リッチテキストエディタなどは、npmのような選択肢がありません。Leptosの版に追いつけていないクレートもあります。
- コンテンツ中心のサイトで初回表示を軽くしたい:カウンター1つでWASMはgzip後も約43KBで、これにJavaScriptが加わります。静的なページが中心なら、WASMを読み込まない構成のほうが軽くなります。Leptosにも、操作が必要な部分だけをWASMに含める islands 方式がありますが、その分だけ構成と学習の手間が増えます。
反対に、サーバー側が既にAxumなどのRustで書かれていて、型をフロントエンドと共有したい場合は、server関数のおかげでAPIの型定義を二重に持たずに済みます。この場合はLeptosを選ぶ理由が最も強くなります。
Leptosに関するよくある質問
Leptosの読み方は何ですか?
「レプトス」と読みます。クレート名も leptos で、cargo add leptos で追加します。
LeptosとDioxusはどちらを選べばよいですか?
Webアプリだけを作るならLeptos、Web・デスクトップ・モバイルを1つのコードで作るならDioxusです。Leptosはシグナルで変わった箇所だけを更新する方式、Dioxusは仮想DOMで差分を取る方式で、どちらもserver関数とSSRを持っています。
LeptosとYewの違いは何ですか?
Yewは仮想DOM方式で、状態が変わるとコンポーネントを再実行して差分を取ります。Leptosはコンポーネントを1回だけ実行し、シグナルを読んでいる箇所だけを更新します。Yewは0.23.0(2026年3月公開)以降版が上がっていません。
cargo-leptosとTrunkはどう使い分けますか?
ブラウザだけで動くCSRのアプリはTrunk、サーバーで描画するSSRのアプリはcargo-leptosを使います。cargo-leptosはサーバー用とWASM用の2回のビルド、Tailwindの処理、開発中の自動再読み込みをまとめて行います。
Leptosの最新バージョンはいくつですか?
2026年10月17日時点の安定版は0.8.22です。0.9系はリリース候補の0.9.0-rc0が公開されています。最新の版は crates.ioのleptosのページで確認できます。