English

axum の上で動く Web フレームワーク

画面もその動きもサーバーも、すべて Rust で

cargo build だけでビルドでき、Node も wasm も要りません。画面とサーバーの食い違いはコンパイルエラーになります。

ブラウザ向けのビルドが不要

cargo build だけでビルドでき、Node も wasm も要りません。gzip で約 14 KB のランタイムをビルド済みの状態でクレートに埋め込んでいます。

画面からサーバーまで型で検査

シグナルの名前、式、アクション、リンクは Rust の型から作られます。ハンドラのないページへのリンクや、パスのパラメータやクエリがページに合わないリンクは、画面とサーバーのほかの食い違いと同じくコンパイルエラーになります。

楽観的更新に追加のコードが要らない

文はブラウザの中で画面を変え、一緒に送ったアクションの呼び出しはそのあとでサーバーに届きます。サーバーの応答を待たずに画面を変える処理を、追加のコードなしで書けます。

スタイル、コンポーネント、翻訳も

hypersig はフレームワーク本体です。同じリポジトリにある 3 つのクレートが、型付きのスタイル、アクセシブルなコンポーネント、型で検査する翻訳を足します。それぞれにドキュメントがあります。

ページを Rust で書く

ページの状態はシグナル構造体が持ちます。Counter::count() はそのフィールドへの型付きの参照で、count.set(count + 1) はクリックのたびにブラウザがサーバーに問い合わせずに実行する文です。サーバーがカウントを入れた状態でページをレンダリングするので、ページはランタイムを読み込む前から表示されます。

実行結果

counter.rs
use hypersig::html::{div, span};
use hypersig::style::Style;
use hypersig::style::pattern::hstack;
use hypersig::style::value::{FontWeight, TextAlign, rem};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Counter {
    count: i32,
}

pub fn page() -> Page<Counter> {
    let count = Counter::count();
    Page::new(
        "カウンター",
        Counter { count: 0 },
    )
    .child(
        div()
            .css(hstack(rem(1)))
            .child(
                button(ButtonVariant::Outline, ButtonSize::Default)
                    .on_click(count.set(count - 1))
                    .text("-1"),
            )
            .child(
                span()
                    .css(
                        Style::new()
                            .min_width(rem(3))
                            .text_align(TextAlign::Center)
                            .font_size(rem(1.5))
                            .font_weight(FontWeight::Number(600.0)),
                    )
                    .bind_text(count.to_text()),
            )
            .child(
                button(ButtonVariant::Default, ButtonSize::Default)
                    .on_click(count.set(count + 1))
                    .text("+1"),
            ),
    )
}

フレームワークの比較

hypersig に近いのは Topcoat と Datastar です。どちらもブラウザの中にシグナルを持ち、ブラウザ向けのビルドなしにサーバーから画面を更新します。Topcoat はブラウザで動く式も Rust で書きます。

Topcoat と Datastarhypersig
シグナル名と URL(Datastar)HTML の属性に文字列で書きます。打ち間違いはブラウザが実行するまで分かりませんRust の値から書きます。打ち間違いがあると cargo build が止まります
リンクのパラメータ(Topcoat)href! が検査するのはハンドラだけです。パスパラメータが足りないか多いと、ページを描くときにパニックし、リクエストは 500 を返しますRoute がパラメータを型の付いた値で受け取ります。間違えるとコンパイルエラーになります
一覧への追加(Topcoat)式から項目を足せないので、サーバーが答えて描き直したときに一覧が変わります一覧をシグナルに持つので、サーバーが答える前にブラウザが変更を表示します
hypersigTopcoatReactNext.jsTanStack StartNuxtDatastar
画面を書く言語RustRustTypeScriptTypeScriptTypeScriptTypeScript と Vue のテンプレートHTML の属性
サーバーを書く言語RustRust任意TypeScriptTypeScriptTypeScript任意
ブラウザ向けのビルドが不要はいはいいいえ(Vite)いいえ(Node のバンドラー)いいえ(Vite)いいえ(Vite)はい
送る値の型をコンパイル時に検査はいはいいいえはいはいいいえいいえ
リンクの行き先をコンパイル時に検査はいハンドラだけ(パラメータは実行時)いいえ(ルーターなし)設定で有効化(typedRoutes)はい実験的(typedPages)いいえ
ブラウザの中で状態を持てるはいはいはいはいはいはいはい

ベンチマーク

追加、チェック、削除をサーバーに保存する 2000 件の TodoMVC を、Apple M4 Max の Chrome 155 で 3 回測った中央値です。CPU を 4 倍に減速し、通信のたびに 50 ms の遅延を足しています。測ったアプリと計測の仕組みは bench/compare にあります。

hypersigTopcoatReactNext.jsTanStack StartNuxtDatastar
JS(gzip)19.2 KB14.8 KB67.1 KB133.3 KB119.5 KB59.2 KB13.1 KB
First Contentful Paint186 ms165 ms196 ms198 ms193 ms177 ms182 ms
Total Blocking Time0 ms313 ms5 ms10 ms0 ms29 ms16 ms
Interaction to Next Paint24 ms24 ms24 ms24 ms24 ms16 ms16 ms

同じページを hypersig::style、Tailwind CSS、Panda CSS、StyleX、vanilla-extract でスタイルした計測は、スタイル のページにあります。