㊙️

フロントエンドエンジニアがWASM(Rust)でWebアプリを作ってみた

2021/12/04に公開

この記事について

この記事はアドベントカレンダー2021 Rust 四日目の記事です

概要

フロントエンドエンジニアである筆者がWASM(Rust)でWebアプリを作ってみたので、

  • その際の技術選定(主にビルド・開発補助周り)について
  • 作ってみた雑感、特に「ぶっちゃけフロントエンドRust[1]ってどうなの?」という点

について書いていこうと思います。

TL,DR

  • 2021年12月現在フロントエンドでのRustの採用は技術的に可能だがエコシステムが未発達なのでそれなりに技術力とコストを要求される
  • とはいえ伸び代はかなりあるのでエコシステムが頑張ってくれれば、数年後には十分一般的な選択肢になってるかも(なってて欲しい)

主な対象読者

  • フロントエンドに携わる機会がある方々

この記事を読んでいただくことで・・・

  • フロントエンドという文脈におけるWASMの現状を知ってもらう
  • フロントエンドをRustで実装するという選択肢を持っていただく
  • サンプルの技術選定について理解することで、読者が同様なスタックを使う時の一助になる
  • 筆者の採用技術に問題があったりよりベターなものがあった場合、教えていただける(笑)

余談:筆者はなぜRustを?

時間的に余裕ができたので何らかのWebアプリを1つ作ってみようと思ったのですが、今更TypeScript + Next.jsで作っても面白味がないと感じ、思い切ってWASM(Rust)を採用しました。

このアプリの概要及びインフラ構成

URL

https://pick-role.web.app/

リポジトリ

https://github.com/itskihaga/exprocess-rust-sample

何これ

役職振り分けアプリです。
簡単に言ってしまえば人狼ゲームから「役職の振り分け」だけを抜き出して人狼以外にも使えるように汎用度を高めたアプリです。
同一ルームに参加した(=サイトが発行した1つのURLにアクセスし参加表明した)複数人で同時に使用可能であり、役職は振られた本人の画面にしか表示されないのでお互いがどの役職かを隠しながら色々遊んだりができます。

何に使う?

普段友達と遊んでるゲームにオリジナルで正体隠匿的な要素を追加したいときに使えます。

アプリの構成

ざっくり書くとこんな感じ。

  • 静的なHTML + JS + Wasm + その他デザイン用の静的ファイルをFirebase Hostingにてホスティング
  • UIの構築は全てブラウザにて行う(クライアントサイドレンダリング)
  • 状態は全てfirestoreに保存し、ブラウザから直接、参照・更新・購読を行う

技術スタック

今回採用した技術スタックを概要と使用用途、(同様なライブラリ等があった場合は)選定理由、そして評価の観点でまとめました。
※かなりの量を書いていきます。詳細な技術にさほど興味なく読むのが辛いという方は次の章までスキップしていただいた方が良いかも。

インフラ

firestore

https://firebase.google.com/docs/firestore

用途
  • 同一ルームにいる複数人で状態を同期する
選定理由
  • SDKを使えば苦せず状態の同期を実装できる
  • 基本無料
  • 娯楽用途なので(ルームにアクセスしてくるのは善意のユーザーのみなので値の改竄等のリスクがほぼない)サーバーサイドでビジネスロジックを持つ必要がない
  • 最低限のセキュリティルールであれば宣言的なフォーマットで設定できる(ルームを跨いでのいたずら・不正行為は防ぐ必要があるため)
評価
  • ○ 機能が豊富で基本的には困らない

firebase hosting

https://firebase.google.com/docs/hosting

用途
  • Webアプリケーションのホスティング
選定理由
  • firestoreと開発アカウントの管理などを一元に扱いやすい
評価
  • 特になし

Webアプリケーションの実装

yew

https://yew.rs/

  • Rust版Reactなライブラリ
  • マークアップはJSXライクな記法とそれを解釈してくれるマクロで行う
  • 状態管理はTEA[2]を真似ており、ViewからMsgを送信し、Model(コンポーネント)が受け取ってUpdateするアーキテクチャ
用途
  • マークアップ
選定理由
  • 同様のフレームワークにSeed等あったが一番人気そうなものを選択
評価
  • ○ 機能としてはほぼほぼhooks登場前のReact
  • ○ Reactと違い状態の更新をMessageを介してしか行えないようになっているため素のままのYewでもいい感じに状態管理できる。
  • ○ Callback(リスナーみたいなもの)のAPIが優秀。関数を使ってCallbackを変換していくのがとても書いていて気持ちいい。
  • × JSXと違いマークアップにおけるエディタの支援がない
  • x Scoped CSS周りのデファクトがない
  • × Storybookのような簡易的にマークアップを確認できるツールがない
  • × サーバーサイドレンダリングができない

yew_router

  • YewをSPAとして表示するためのライブラリ
用途
  • SPAとして表示するため
評価
  • ○ React Routerと違いマクロで静的にURLのスキーマの型安全性を検証できる。

firebase SDK(JavaScript)

https://firebase.google.com/docs/firestore/client/libraries?hl=ja

用途
  • フロントエンドからfirestoreへのアクセス
選定理由
  • Rust(WASM)のFirestore SDKが公式にはない。

serde,serde_json

  • JSONシリアライズ・デシリアライズ用モジュール
  • 定義した構造体にマクロを噛ませることでシリアライザとデシリアライザが生成される
用途
  • wasm-bindgenの都合上、Rust<→JavaScript間であまり複雑なオブジェクトの受け渡しができない+firestoreのドキュメントには階層の深いオブジェクトは保存できない[3]ため、firestoreはJSON文字列を格納するようにしており、その際firestoreとRustの値とのやり取りに使用した。
選定理由
  • デファクトがserdeらしい
評価
  • ○ 特に問題なく普通に使える。
  • ○ ゼロランタイムなので環境にも良い。

ビルド

wasm-bindgen

  • WASMから公開されるAPI(ABI)をJavaScriptで扱う際に必要な諸々の実装を肩代わり(ビルド時に生成)してくれる。
評価
  • ○ RustのコードからTypeScriptの型定義コードまで吐き出してくれるのはかなり有能
  • × ただ、RustからJavaScriptの関数をimportする際は型生成されないので注意
    • Rustを中心に据えた設計をしているとRust→JavaScriptの依存関係が発生してしまいその部分の型の安全は諦めなくてはならないので結構痛い

webpack,html-webpack-plugin

用途
  • JavaScriptも実装(firestoreとの接続部分)してる関係上、JavaScriptと.wasmファイル、そして最終的にindex.htmlまでを全て統合する必要があった
選定理由
  • (どうしても.wasmファイルが肥大化してしまい初期描画のボトルネックになりそうだったので)ハッシュ文字列によるCache Bustingやモジュールの実行優先度に基づくChunkへの分割などの細かい最適化をするための行き届いたAPI・プラグインが欲しい
  • 昨今話題になっている「ビルドが高速な」バンドラ・コンパイラは魅力的ではあるが上記最適化の方がビルド時間の短縮よりも優先度が高かった
    • そもそもビルド時間の一番のボトルネックJavaScriptではなくWASMのコンパイルなのでJavaScriptバンドラをこだわっても効果が低い
    • JavaScriptと連携したモノリシックなビルド生成物を使っての動作テストはそこまで頻度が高くない想定(だった泣。もう少しモジュールレベルのテストを充実させていれば・・・)
評価
  • × ビルド速度は遅い

ローカル開発

firebase-tools

https://firebase.google.com/docs/cli?hl=ja

用途
  • firebaseのエミュレータを使ってfirestoreとfirebase hostingを起動し、動作確認をローカルで完結させるため
評価
  • ○ firestoreのエミュレータはかなり良い感じ
  • × firebase hostingのエミュレータは本番と挙動がちょくちょく違う

ローカル開発(UIコンポーネントの開発)

YewにはStorybookのようなUIコンポーネントをバックエンドから切り離して簡易的に表示する手段がないため、それに代わるものを自作しUIコンポーネントを表示させるようにした。

ejs

Node.jsで動くテンプレートエンジン

用途
  • 簡易表示用のindex.htmlファイルの生成
選定理由

serve

Next.jsのVercelが開発している静的ファイルホスティングサーバー

用途
  • ejsで吐き出した.htmlファイルやコンパイルした.wasmファイルをホスティングする
選定理由
  • セットアップが簡単

フロントエンドにおけるRustについて

以下、所感です。

Webフレームワーク

Yew、Seedなどいくつかフレームワークが乱立している印象です。
特にYewはRust版Reactとしての地位を確固たるものにしつつ、TEAライクなアーキテクチャを導入しReact以上に洗練された設計のフレームワークになっていく可能性がありこれからの発展が楽しみです。

パフォーマンス

パフォーマンス的な懸念が出てくるような用件ではなかったため、WASMによって速くなった(orならない)という実感はありませんでした。
寧ろ.wasmファイルのサイズが簡単に数百kbを越えてしまうために、クライアントサイドレンダリングでの初期描画には懸念があります。商用のWebフロントエンドにWASMを採用する際は.wasmファイルの最適化を行える低レイヤに理解のあるエンジニアをきちんと抱えるべきしょう。

開発・ビルド

2021年現在、RustでのWebフロントエンドはNext.jsのようなAll in Oneのスタンダードなフレームワークがありません。
なので開発者が1から全て組み上げていかなければならないのが非常に面倒です。
特にデータアクセスなどの部分でJavaScriptとの連携が必要になった場合、それらを統合するためのビルドパイプラインが煩雑になりがちです。
また、Webフロントエンド開発の特性上、ブラウザでの表示確認のために修正→ビルド→確認のサイクルをかなり沢山回す必要があるため複雑すぎるビルドのパイプラインが開発のボトルネックになる可能性があります。
正直なところ、これらを上手く解決できるレベルの技術力が無ければ開発効率や初速の面で厳しい開発になってしまうかもしれません。

Rustという言語の可能性

ここまで若干のネガキャンもありましたが、筆者個人としてはフロントエンドRustに対してかなり肯定的な気持ちを持っています。それは現状のフロントエンドへの不満によるところが正直大きいです。
現在、フロントエンドの主流となる言語はTypeScriptです。しかし、TypeScriptは実行形態であるJavaScriptが足を引っ張り、言語としての抜本的な成長の可能性を閉ざしてしまっている印象があります。
そんな中Rustは以下の3点からTypeScriptを(フロントエンドで使用する言語として)上回る強い可能性を秘めています。

  • 型安全である[4]
  • マクロを使って処理をコンパイル時に寄せることができる[5]
  • 言語ではなくWASMというバイナリのフォーマットが規格化されているのでブラウザ差分が出づらい

これから先Rust・WASMコミュニティが発展し、前述したネガティブをうまく乗り越えられれば、(万人向けではないとはいえ)返ってくるリターンは大きいはずです。
そのためにも僕らフロントエンジニアが臆せずRustを使ってコミュニティを盛り上げていくことが大切だと思います!!

脚注
  1. ここではランタイムにおいて可能な限りReactなどのJavaScriptのライブラリを採用せず、Rust中心で実装するフロントエンドのこと ↩︎

  2. The Elm Architecture のこと https://guide.elm-lang.jp/architecture/ ↩︎

  3. もちろんオブジェクトを1つのドキュメントに詰めようとせずドキュメントの階層をきちんと切って小さい単位で保存すれば問題にはならないのですが、Rustに実装を寄せたいアーキテクチャの都合上、文字列化した状態でやり取りするのが一番JavaScriptの実装が楽でした。 ↩︎

  4. TypeScriptはどうしても実行時の型を保証できません。開発者は自分が注釈した型と実行時に変数に入ってくる型が同一になるように日々努力する必要があり、そして問題はその努力をしなくてもアプリケーションは動いてしまう可能性があることです。Rustは型がかなり厳格であり、少なくとも数値型の変数に文字列やnullが入り込まないことはコンパイラが確実に保証してくれます。 ↩︎

  5. JavaScriptの世界ではbabelというトランスパイラがよく使用されますが、言語標準ではなくまたランタイムとビルドタイムを隔離する明確な文法がないため色々な面で不安定にならざるを得ないと筆者は考えています。 ↩︎

Discussion