🌿

RustのWasmで仮想DOMなYewの紹介とyew-style-in-rsの使い方

2022/06/30に公開

はじめに

この記事ではRustのWasmで仮想DOMベースのWeb用ライブラリクレートであるYewの紹介と実際に使って見て、そしてYew用のスタイルライブラリであるyew-style-in-rsの導入までを行います。

記事執筆時のRustのバージョンは1.61.0で、yewのバージョンは0.19.3です。

Yewの紹介

公式サイト

Yewは公式サイトによると

Yew is a modern Rust framework for creating multi-threaded front-end web apps using WebAssembly.

とのことです。

マルチスレッドとはありますが、現状ではWebWorkerを使ったマルチスレッドはまだ対応していなかったと思います。
非同期ランタイムのような軽量スレッドを指してマルチスレッドと呼んでいるようで、若干どうなのと思わなくもないですが……。

Yewは仮想DOMベースでコンポーネントベースのWebを記述できるライブラリです。
ReactのWasm/Rust版といった理解をしておけば概ね間違っていない感じです。
Yewはfunction componentやhooks APIなどReactを知っておくと、ほとんどそのままその知識でかけるくらいReactに似ています。
貢献している人の中にたまにVue.jsが好きな人がいるのか、Vue.js的な機能を取り込もうとしている様子が見えたりしますが、概ねReactです。

Yewの競合の紹介

Yewと同じようにWasmで仮想DOMなWeb用のライブラリというのはいくつかあるのでそちらも一言ずつ紹介をしておきます。

  • Percy: SSRまで対応しているWasmとRustでかけるフロントエンドアプリ用クレート
  • Sycamore: WasmとRustでwebアプリを書くリアクティブにフォーカスしたクレート
  • rust-dominator: 仮想DOMを使わない宣言的なDOMのライブラリ
  • Dioxus: WebだけにとどまらないクロスプラットフォームなGUIライブラリ

特にDioxusは触ったことがあるのでもう少しだけ言及をしておきます。

Dioxusは現在活発に開発が続いているクレートです。
Yewなどに比べてもだいぶ開発が活発なようです。
WebのHTMLの技術に乗っかっていますが、TauriというWebViewでデスクトップアプリなどが作れるクレートと同じTaoとWryというライブラリクレートに依存する形で、デスクトップアプリやモバイルアプリまで対応できることを売りにしています。

APIを触ってみた感触としては、DioxusはYewよりマルチプラットフォームを意識して、HTMLなどの知識をなるべく隠蔽しようとしているように感じられました。
イベントなどを触る際も、Yewではweb_sysやjs_sysなどを組み合わせて解決することが多い場面で、Dioxusではそれらに頼ることなくかけるようにラップした構造体を用意しようとしているようです。
これはどちらが良いという問題でもなくて好みの問題があると感じていて、よりRustのネイティブなGUIクレートという感覚で書きたいならDioxusを、そうではなくてあくまでWebを書くんだという意識があるならYewを選ぶと良さそうに思えます。

まだDioxusは発展途上ではありますが、注目しておくべきプロジェクトではあると思われます。

Yewのmasterと0.19.3の間のギャップ

2022年6月11日現在、crates.ioで公開されている最新版のYewは0.19.3です。
GitHubのmasterはこれより開発が進んでいるようで、一部には互換性のない機能開発も行われているようです。
現状ではYewの周辺ライブラリの多くは0.19.3に対応していて、masterでは動かないライブラリがあるため、この記事では0.19.3を使って行きます。
ただし、DiscordのYewのコミュニティでこの機能はないのかと聞くとmasterを試してみてくれと言われることもあるようなので、masterと最新版の間にギャップが有ることを覚えておくと良さそうです。

Yewを使ってみる

早速Yewを使ってみます。
YewはTrunkと合わせて使うと非常に簡単に始めることができます。

TrunkやRustのWasm周りについては別で記事を書いたのでよろしければ参考にしてください。

まずはcargo newして、そのあとindex.htmlを作成します。

- src
  - main.rs
- Cargo.toml
- index.html
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Yew App</title>
  </head>
</html>

このindex.htmlがTrunkのエントリーポイントとなります。

次にcargo add yew log wasm-loggerとしていくつかの依存クレートをCargo.tomlに書き込みます。

最後にmain.rsに次のように書いてtrunk serveすればHello worldと表示されるはずです。

main.rs
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
  html! {
    <div>
      <h1>{"Hello, world!"}</h1>
    </div>
  }
}

pub fn main() {
  wasm_logger::init(wasm_logger::Config::default());
  log::debug!("Hello Yew!");

  yew::start_app::<App>();
}

wasm-logger及びlogクレートは必要がなければ入れなくても大丈夫なロギング用のライブラリです。
main関数でyew::start_app::<App>()としている部分がYewを起動している部分です。

App自体は関数コンポーネントを使って定義しています。
app関数に#[function_component(App)]と書くことで、Appというコンポーネントを関数で定義できます。
コンポーネントの関数はHtml型を返します。
html! {}マクロを使ってまるでJSXのようにHtml型を作成できます。

自分の手でJavaScriptを記述せずとも、最低限のHTMLとあとはRustを書くだけでこんなにすんなりとWebが書けるのは感動ですね。

関数コンポーネントと構造体コンポーネント

Yewには関数コンポーネントと構造体コンポーネントがあります。
構造体コンポーネントはElmっぽいテイストでコンポーネントを定義できます。
関数コンポーネントはReactの関数コンポーネントに影響を受けていて、こちらのほうが新しい方法となっています。

yew-hooksなどの便利なライブラリは関数コンポーネント専用だったりするので、今となっては構造体コンポーネントで記述することはないでしょう。
関数コンポーネントの書き方を選んでおけばとりあえず問題はありません。
とは言えドキュメントにはまだ構造体コンポーネントの書き方が残っているので注意が必要です。

Props

Reactと同様にコンポーネントに渡すプロパティとしてPropsというものがあります。
YewではPropsにしっかりと型がつくのでとても体験が良いです。

PropsPartialEqを実装する型に対してPropertiesderiveすることで作れます。

use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
  pub is_loading: bool,
}

このPropsの参照を引数として受け取るように関数コンポーネントを書き換えることで、Propsをコンポーネントに渡せます。

main.rs
use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
  pub is_loading: bool,
}

#[function_component(HelloWorld)]
fn hello_world(props: &Props) -> Html {
  html! { <>{props.is_loading.clone()}</> }
}

#[function_component(App)]
fn app() -> Html {
  html! {<HelloWorld is_loading=true />}
}

pub fn main() {
  yew::start_app::<App>();
}

このHelloWorldコンポーネントはis_loadingにboolの値を渡さないとエラーとなります。

error[E0599]: no method named `build` found for struct `PropsBuilder<PropsBuilderStep_missing_required_prop_is_loading>` in the current scope
  --> src\main.rs:16:13
   |
3  | #[derive(Properties, PartialEq)]
   |          ---------- method `build` not found for this
...
16 |     html! {<HelloWorld />}
   |             ^^^^^^^^^^ method not found in `PropsBuilder<PropsBuilderStep_missing_required_prop_is_loading>`
   |
   = note: the method was found for
           - `PropsBuilder<PropsBuilderStepPropsBuilder>`

オプショナルにしたい場合はOptionでラップしてやるか、#[prop_or()]でデフォルト値の設定、あるいは#[prop_or_default]でデフォルト値を利用可能です。

#[derive(Properties, PartialEq)]
pub struct Props {
  pub is_loading: Option<bool>,
}
#[derive(Properties, PartialEq)]
pub struct Props {
  #[prop_or(true)]
  pub is_loading: bool,
}
#[derive(Properties, PartialEq)]
pub struct Props {
  #[prop_or_default]
  pub is_loading: bool,
}

Children

pub children: Childrenというフィールドを設けることで、Htmlの子要素を受け取るようなコンポーネントも作成できます。

#[function_component(App)]
fn app() -> Html {
  html! {
    <HelloWorld>
      <span>{"Hello"}</span>
      <h1>{"Yew"}</h1>
    </HelloWorld>
  }
}

#[derive(Properties, PartialEq)]
pub struct Props {
  pub children: Children,
}

#[function_component(HelloWorld)]
fn hello_world(props: &Props) -> Html {
  html! {
    <div>
      { for props.children.iter() }
    </div>
  }
}

Callback

イベントのコールバックにはCallbackという関数をラップしたような型を使います。

main.rs
use yew::prelude::*;

#[function_component(HelloWorld)]
fn hello_world() -> Html {
    let onclick = Callback::from(|_| log::debug!("Clicked!"));
    html! { <button onclick={onclick}>{"Click Me!"}</button> }
}

#[function_component(App)]
fn app() -> Html {
    html! {<HelloWorld />}
}

pub fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    log::debug!("Hello Yew!");

    yew::start_app::<App>();
}

Hooks

関数コンポーネントに状態をもたせたり、その他いろいろな機能を持たせるには、Reactと同様にHooks APIを利用します。

use_state

use_stateはちょうどReactのuseStateに対応します。
Reactとは違い、値とsetState関数のペアではなく、
Derefで値に変換可能でsetメソッドを持った構造体が返されます。

例えば次のようにしてコールバックでインクリメントするコードが書けます。

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    let count = use_state(|| 0);
    html! {
        <div>
            <button onclick={
                let count = count.clone();
                Callback::from(move |_| {
                    log::debug!("Clicked!");
                    count.set(*count + 1);
                })
            }>{"Increment"}</button>
            <p>{format!("Count: {}", *count)}</p>
        </div>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

setメソッドを呼び出すたびに再レンダリングが走ります。
同じ値が渡された場合に再レンダリングを抑制したい場合、次のuse_state_eqや、ues_mut_refを使うと良いでしょう。

use_state_eq

こちらはuse_stateとほぼ同様ですが、PartialEqによって同値と判断される値がsetされた場合には再レンダリングを走らせないという特徴があります。
必要があればPartialEqをPropsに独自実装することで、再レンダリングのタイミングなどを変更することが可能です。

use_effect

こちらは副作用を起こすことを目的として使われるHookです。
どちらかといえば依存する値を設定できる次のuse_effect_with_depsのほうがよく使います。

use_effect_with_deps

依存する値を指定できるuse_effectです。
依存している値がPartialEqで比較して前回と異なっているときだけ、use_effect_with_depsの中身が実行されます。
use_effect_with_depsの第二引数に()を渡すことで、コンポーネントが最初にマウントされるときと、削除されるときのみに実行される内容を書くことができます。

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    use_effect_with_deps(
        |_| {
            log::debug!("mount App");
            || ()
        },
        (),
    );
    html! {
        <div>
            <h1>{ "Hello, world!" }</h1>
        </div>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

use_effect_with_depsの引数に渡すクロージャの戻り値のクロージャはアンマウント時のクリーンアップ用のコードを書くことができます。

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    use_effect_with_deps(
        |_| {
            log::debug!("mount App");
            || log::debug!("unmount App")
        },
        (),
    );
    html! {
        <div>
            <h1>{ "Hello, world!" }</h1>
        </div>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

use_effect_with_depsの第二引数に任意の値を渡すことで、その値が変更された場合のみに副作用を実行することもできます。

use_mut_ref

値を一回のレンダリングを超えて保持しておきたい場合に使えます。
ReactのuseRefの機能のうち、任意の値の参照を保持しておく機能がuse_mut_refに、DOMへの参照を手に入れる機能がuse_node_refに切り分けられています。

use_node_ref

DOMにアクセスするためのリファレンスを手に入れられます。
取得したいDOM要素のrefuse_node_refの戻り値を渡すことで、そのDOM要素を取得できます。

例えばuse_effect_with_depsと組み合わせて次のようにイベントリスナを登録できます。

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlDivElement;
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    let div_ref = use_node_ref();
    use_effect_with_deps(
        {
            let div_ref = div_ref.clone();
            move |_| {
                let div = div_ref.cast::<HtmlDivElement>().unwrap();
                let closure = Closure::wrap(Box::new({
                    let div = div.clone();
                    move || {
                        div.set_inner_html("Hello, world!");
                    }
                }) as Box<dyn FnMut()>);
                div.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())
                    .unwrap();
                move || drop(closure)
            }
        },
        (),
    );
    html! {
        <div ref={div_ref} style="width: 300px; height: 150px; background: cyan;"></div>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

イベントリスナの登録とClosure周りについては、wasm_bindgenの文書が参考になります。

ただし、上記のようなイベントの登録処理は、yew-hooksuse_eventを利用すると、Closureを作ることなくより簡単に書けます。

use web_sys::HtmlDivElement;
use yew::prelude::*;
use yew_hooks::use_event;

#[function_component(App)]
fn app() -> Html {
    let div_ref = use_node_ref();
    use_event(div_ref.clone(), "click", {
        let div_ref = div_ref.clone();
        move |_: Event| {
            let div = div_ref.cast::<HtmlDivElement>().unwrap();
            div.set_inner_html("Hello, World!");
        }
    });
    html! {
        <div ref={div_ref} style="width: 300px; height: 150px; background: cyan;"></div>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

その他

その他にもいくつかHookは用意されています。
APIのドキュメントを読むと良いでしょう。
上で紹介しなかったHookには次のようなものがあります。

  • use_memo
  • use_callback
  • use_reducer
  • use_reducer_eq
  • use_context
  • use_force_update

また、3rd party製のyew-hooksというライブラリもあり、こちらもオススです。

Yewのスタイル事情

YewはStyle周りについてはまだ発展途上です。

現状でも次のようないろんなライブラリがポートされている様子がawesome-yewを見るとわかります。

用意されたコンポーネント一式ではなく自分でスタイルを組みたい場合、愚直にCSSファイルを別に書くこともできます。
trunkはSassにも対応をしており、SassやScssを読み込むことができます。

また、styled components的なライブラリのfutursolo/stylist-rsMatchaChoco010/yew-style-in-rsあたりを使うのも良いでしょう。

yew-style-in-rsを使う

MatchaChoco010/yew-style-in-rsは私が制作し管理しているOSSのライブラリです。
ここでは、このライブラリの使い方を軽く説明します。

このライブラリはcss!マクロなどを使うことで、コンポーネントに閉じたCSSをコンパイルタイムに生成します。
生成されたstyle.cssはビルドするとoutput directoryに出力されます。

trunkを使う場合、output directoryからtrunkのディレクトリにstyle.cssをコピーする手間がかかるので、おすすめの構成として、Exampleと同じようにRustのworkspaceでstyle.cssをコピーするコマンドを別に作ります。

- app/
  - src/
    - main.rs
  - Cargo.toml
  - index.html
- post_build/
  - src/
    - main.rs
  - Cargo.toml
- Cargo.toml
Cargo.toml
[workspace]
members = [
  "app",
  "post-build",
]
post-build/main.rs
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    // Get output directory
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let manifest_dir = Path::new(&manifest_dir);
    let target_dir = manifest_dir.parent().unwrap().join("target");
    let profile = env::var("TRUNK_PROFILE").unwrap();
    let output_dir = Path::new(&target_dir)
        .join("wasm32-unknown-unknown")
        .join(profile);

    // Get staging directory
    let staging_dir = env::var("TRUNK_STAGING_DIR").unwrap();
    let staging_dir = Path::new(&staging_dir);

    // Copy *.css from the output directory to the staging directory
    for entry in fs::read_dir(output_dir).unwrap() {
        let entry = entry.unwrap();
        let path = entry.path();
        let ext = path.extension();
        if let Some(ext) = ext {
            if ext == "css" {
                let dist_path = staging_dir.join(path.file_name().unwrap());
                fs::copy(path, dist_path).unwrap();
            }
        }
    }
}

このpost-buildで生成するコマンドを、Trunk.tomlを作ってtrunkのhookに追加します。

app/Trunk.toml
[[hooks]]
stage = "post_build"
command = "cargo"
command_arguments = ["run", "--release", "-p", "post-build"]

これでtrunkでビルドされるたびに、output directoryにあるcssファイルがtrunkのディレクトにコピーされます。

さて、yew-style-in-rsを使うにはyew-style-in-rsを依存に追加するだけで良い、と言いたいところですが、2022/06/30現在、依存解決の問題でクレートが壊れています。

Yew 0.19.3本体が依存しているglooのバージョンが古く、0.4.2となっています。
このglooにはgloo-netは同梱されておらず、またgloo-utilsの0.1.2を利用しています。
一方で、yew-style-in-rsはglooの最新の0.7.0を利用しております。
この最新の0.7.0のglooはgloo-netとgloo-utilsの0.1.4が入っています。
そして、このgloo-netはどうもgloo-utilsの0.1.2には対応しておらず、gloo-utilsの0.1.4が必要なようです。
glooの0.7.0とyewを同時に使うと、gloo-utilsの0.1.2と0.1.4に互換性があるという判定がなされて、glooの0.7.0の中のgloo-utilsがyewの方のバージョンに合わせられて、gloo-utilsの0.1.2しか入らないため、ビルド時にエラーとなってしまいます。
少し前までは問題なくyew-style-in-rsを利用してビルドできていたので、最近のglooのアップデートに伴うgloo-netの0.1.4のアップデートが互換性を破壊しているものと思われます。
互換性を破壊しているのにパッチバージョンしか上げていないことが問題であるとは思いますが、gloo-netやgloo-utilsなどのgloo一式はセットで使われることを前提としているので、見落としがちではあるかもしれない。
今回のように複数のglooが混ざるケースでうまくビルドできなくなるようです。

解決のために、Cargo.tomlにgloo-utilsのバージョンを指定して入れようと思います。

[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
yew = "0.19.3"
wasm-logger = "0.2.0"
yew-style-in-rs = "0.4.1"
gloo-utils = "0.1.4" # 依存解決のために必要

次に、yew-style-in-rsの使い方を説明していきます

コンポーネントにスタイルを当てる場合、style!マクロ内部でcss!を使います。
通常のスタイルシートに加え、 CSS Nestingの記法も使えます。

app/main.rs
use yew::prelude::*;
use yew_style_in_rs::style;

#[function_component(Component)]
fn component() -> Html {
    style! {
        let css = css!{r#"
        width: 300px;
        height: 150px;
        background-color: cyan;

        & h1 {
            color: red;
        }
        "#};
    }
    html! {
        <div class={css}>
            <h1>{"Hello, world!"}</h1>
        </div>
    }
}

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
            <Component/>
            <Component/>
        </>
    }
}

pub fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

これでビルドするとstyle.cssが生成されます。
次のようにhtmlにstyle.cssへのリンクを貼ることでスタイルのあたったウェブサイトが見れます。

index.html
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8"/>
    <title>yew-style-in-rs</title>

    <!-- async load style -->
    <link rel="stylesheet" href="./style.css" media="print" onload="this.media='all'">
  </head>
</html>

style!内のcss!マクロで定義したスタイルは、手続きマクロの黒魔術によって、コンパイル時にランダムなクラス名でstyle.cssに書き込まれ、そのランダムなクラス名に置き換えられます。
そのため、コンパイル時にスタイルが決まってスタイルを実行時に変更する必要がない部分では、このcss!を利用することで、実行時のstyle要素の追加の実行コストやCSSの文字列をWasmに保持することによるWasmファイルの肥大化を防ぐことができます。

動的にスタイルを変更する必要のある部分では、dyn css!を利用します。

app/main.rs
use yew::prelude::*;
use yew_style_in_rs::style;

#[derive(PartialEq, Properties)]
struct Props {
    pub background_color: String,
}

#[function_component(Component)]
fn component(props: &Props) -> Html {
    let background_color = &props.background_color;
    style! {
        let css = css!{r#"
        width: 300px;
        height: 150px;

        & h1 {
            color: red;
        }
        "#};
        let dyn_css = dyn css!{r#"
        background-color: ${background_color};
        "#};
    }
    html! {
        <div class={classes!(css, dyn_css)}>
            <h1>{"Hello, world!"}</h1>
        </div>
    }
}

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
            <Component background_color="cyan"/>
            <Component background_color="green"/>
        </>
    }
}

pub fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::start_app::<App>();
}

screenshot

この他にyew-style-in-rsには、CSSアニメーション用の機能なども含まれています。

SSRとhydration機能

Yewは0.19.3現在SSRを利用可能です。
ただしこれは完全にスタティックにサーバーサイドでレンダリングすることしかできません。
いわゆるhydrationという、SSRした要素にあとからバインドして動かす機能はまだ実装中です。
staticにgenerateしておいたコンテンツに対してあとからWasmを読み込んで機能を注入することができないので、SSRで生成したコンテンツは、Static Site Generatorなどの完全スタティックになる用途にしか使えなさそうに思います。

YewのDiscordのコミュニティでは、hydrationをYewのバージョン1.0.0に向けて実装しようとか、その前に0.20.0とか0.21.0とかで実装をして安定してから1.0.0に使用などという話があ出ているのを観測しました。

完全にスタティックにすることができない場合(Static Site Generatorではないほとんどのアプリの場合が当てはまるでしょう)、SSRは使えないことになります。
SSRにhydrationすることで、ファーストビューを素早く出すということが、Reactなどではよく行われるようですが、そのような用途としてSSRができるようになるのはもう少し先と思われます。
現状ではSSRできないので、Wasmが完全にロードされるまで真っ白なページが表示されるような状況です。
wasmのファイルサイズ代わりとでかいので3G slow回線とかのような遅い回線だとなかなか厳しいという印象です。

SSRのhydrationが来れば、Yewも実践でもう少し使いやすくなるのかなと思いますが、いかんせんこの仕組が来るまでは実務でWebサイトを作るには厳しいかなという印象。
早くhydrationが実装されると良いですね。

Wasmのファイルサイズ縮小テクニック

hydrationがない現状では特に(そしてhydrationができたあとでも)、Wasmのファイルサイズの大きさがかなり課題になりそうに思います。
Wasmのサイズを小さくビルドする方法についてメモしておきます。

まずはルートのCargo.tomlに次の記述をします。

Cargo.toml
[profile.release]
panic = 'abort'
codegen-units = 1
opt-level = 'z'
lto = true

それぞれWasmのファイルサイズを減らすのに役に立ちます。

Trunkからwasm-optを呼び出してWasmサイズを小さくする機能も有効化しておくと良いでしょう。

index.html
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8"/>
    <title>Title</title>

    <link rel="rust" data-trunk data-wasm-opt="z"/>
  </head>
</html>

Wasmのファイルサイズ

ファイルサイズの参考までに、先のdyn css!の例で144KB程度の大きさのWasmになりました。
Reactなどと比べて特別びっくりするほど実用不可能なほど巨大というわけではないものの、ここまで小さい例でそこそこの大きさなのでなかなか辛そう。
なにか凝った実装などをするとWasmサイズが膨らんだりもするので、なかなか難しいものです。

Wasmの分割ロード

WebにおけるJavaScriptではファーストビューに必要なJavaScriptだけ最初に読んで、残りは必要になってから非同期に読み込むということも行ったりらしいです。
できることならば、巨大なWasmの塊をダウンロードし終わるまで何も描画できないというのを避けるために、Wasmを分割して段階的に読み込めたりするのが理想です。
しかし、Rustで生成するWasmの場合はそのようなことを行うのがまだ難しそうです。
Wasmには複数のWasmにプログラムを分割する仕組みとして、動的リンクする仕組みが、emscriptenの独自拡張的立ち位置で用意されていたりはします。
RustのWasmで同じようなことをやるにはLLVMのリンク周りに作業が必要という話を何処かで聞いた覚えがあります。

終わりに

Yewの紹介、yew-style-in-rsを使う例、そしてSSR hydrationへの展望を書きました。

またYewの生成するWasmのファイルサイズを小さくする方法なども書きました。
YewをWebで使っていく場合にはまだしばらくはWasmのサイズと戦う必要がありそうです。

ファイルサイズについて、個人の感想ではありますが、一方でWasmのサイズを気にしなくて良い、ElectronやTauriのローカルアプリ制作ではガンガンYewを使っていけるかなという手応えはあります。
最初のうちは、私はそのあたりの用途で使っていこうと考えています。

なんにしても、Trunkを使うことで、スクリプト言語でも書くように非常にお手軽に、JSを一切(自分の手では)書くことなくRustでこれだけ動くものがサクッとできるのは、なかなか感動モノですね。

Discussion