Flutter WebのWasmバイナリをFirebase Hostingで動かしてみた
CHANGELOG
- 2025.01.27
- 2024.05.14
- Google I/O 2024にて Flutter stable 3.22リリースとともに WasmサポートもStable となったため関連箇所を修正
背景
2023年5月10日の Google I/O 2023 にて、Flutter 3.10/Dart 3 の正式リリースが発表されたのと同時に、Flutter の Web Assembly(以下、Wasm)対応もプレビュー版ですが公開されました。
Flutter の Wasm に関するドキュメントは「Support for WebAssembly (Wasm)」というタイトルで、今年の3月辺りから少しずつ更新されておりましたが、今回からビルド方法などの手順が詳細に記載されるようになりました。
こちらは Flutter 3.10 における Flutter Web のアップデート内容の紹介ですが、Wasm は 「What's next for Web」 というセクションで紹介されています。現在の Flutter Web サポートの全体像が見えるので非常にオススメな動画です。
今回は、この Flutter Web の Wasm ビルドを Firebase Hosting 上で動かしてみて、CanvasKit や HTML レンダラーの従来形式と比較しながら、その特徴について気づいたことなど記載します。
Dart と Wasm
これまで、Wasm バイナリにコンパイルできる言語は C や Rust のようなガベージコレクション(GC)を利用しない低級言語のみであり、GC を要する言語についても プロポーザル などで標準化に向けた仕様策定が進められていました。
その後、今年1月末に開催された Flutter Forward 2023 にて、Flutter/Dart の Wasm サポートが発表され、GC を必要とする言語での初のサポートとなりました。これには Chromium V8 の開発チームとの連携などが不可欠で、Chrome の開発チームと協力して開発したことでいち早いサポートができたのだと思っています。現在、この WasmGC は Google Chrome や Firefox で安定版として提供されており、Safariも今年の4月に開発着手 しており、各ブラウザが出揃ってきた感が伺えます。
Flutter/Dart/WasmGC の詳細について今回は割愛しますが、今年3月に開催された WASM I/O で説明されているので参考にしてみると良いと思います(Flutter, Dart, Wasm-GC - Wasm I/O Conference - March 23, 2023 - [shared publicly] - Google スライド)。
なにが嬉しいのか
Wasm 対応する一般的なメリットは処理の高速化です。本来 JavaScript で処理しないといけなかったものがバイナリで処理されるため、それだけでもイメージしやすいと思います。身近な例では、Google Meetsの背景ぼかし処理 や Figmaの読み込み速度3倍 などが挙げられます。
最近公開された Tim 氏によるこちらの記事においても、「初期のベンチマークでは、実行速度が 3 倍ほど向上」と記載されており、高い性能で処理できていることが伺えます。
In some early benchmarks, we’ve seen a boost of 3× for execution speeds, which translates into yet richer web-based experiences. And Wasm couples this with easier integration with code written in other languages like Kotlin and C++.
またユーザビリティ観点では、Wasm はネイティブコードに近い性能なよりリッチで快適なウェブベースの体験を提供してくれると記載されており、これまで Flutter のビルド対象となる Platform の中で個人的に唯一微妙だなと感じていた Web アプリが、Wasm によって一気に普及しそうだなと思っています。
“WebAssembly excites us with its potential to bring the performance of native code to the web.”
Wasm にビルドする
ビルドは至って簡単です。以上です。
flutter build web --wasm
ただし、実行するには少し制約があります。
端的に言うと「最新の master
チャンネル」と「最新の Google Chrome ブラウザ」が必要です。
注意事項
master
チャンネルのみ- Google Chrome(Chromium ベースのブラウザ)のみで、Version 112 以降のみが対応
- Google Chrome ブラウザの以下 2 つの flags(
chrome://flags/
)の有効化が必要enable-experimental-webassembly-stack-switching
enable-webassembly-garbage-collection
- サポートはビルドのみ(
flutter run
や DevTools は非対応) - ブラウザと JavaScript API をターゲットにするため、
dart:html
やpackage:js
を使用している場合はコンパイルできない- url_launcher | Flutter Package など有名パッケージも該当
私はこちらの環境で実行しました。
❯ fvm flutter --version
Flutter 3.8.0-16.0.pre.18 • channel master • https://github.com/flutter/flutter.git
Framework • revision 0c1ed75915 (3 months ago) • 2023-02-23 10:39:41 +0000
Engine • revision d4bae2887e
Tools • Dart 3.0.0 (build 3.0.0-266.0.dev) • DevTools 2.22.1
正常にビルド完了し、後述の Firebase Hostingへデプロイ などを参考にホスティングした後、Developer Console の Network で確認すると、JavaScript に加え Wasm バイナリが実行されていることが確認できます。
WasmビルドされたFlutter Web |
---|
![]() |
ビルド方法の比較
今回の Wasm 対応が追加されることで、Flutter Web のビルドモードが Defaultと WebAssemblyの2種類となります。Default は従来でいうところの CanvasKit(--web-renderer canvaskit
と指定していたビルド方法)と一緒です。HTML レンダラーが正式になくなったことで CanvasKit でのビルドをデフォルトの位置づけにしたようです。
Build options | Compiled | Renderer |
---|---|---|
--wasm |
Wasm、JavaScript | skwasm or canvaskit |
指定なし | JavaScript | canvaskit |
ちなみに、--wasm
が Wasm だけでなく JavaScript にもコンパイルされるのは、Wasm では DOM を直接触れず JavaScript を介して利用するなど、Web 周辺の技術を扱うには JavaScript が必要なためです。Wasm 自体も JavaScript を代替するものではなく、共存していく形が業界標準のはずです。
下記の図が出力結果の比較です。差分はごく僅かで、以下の 2 ファイルが Wasm ビルド時には生成されていることがわかります。
canvaskit |
skwasm |
---|---|
![]() |
![]() |
やや理解に自信がない部分もありますが、それぞれの役割はざっと以下だと思います。
-
main.dart.wasm
- ロジックなどのアプリケーション処理
- 従来のビルド形式では
main.dart.js
で実装されていた処理
-
main.dart.mjs
-
main.dart.wasm
からコンパイルしたWebAssembly.Module
を実行できるようにしているランタイム(?) - 内部で
dart2wasm
を使っているぽい
-
Wasm ファイルが読み込まれるまで
前述の通り、Wasm の違いは .mjs
と .wasm
ファイルが存在している程度です。他にも canvaskit
の skwasm
系のファイルがなくなっていりしていますが今回は割愛します。
従来のビルド形式同様、index.html
から flutter.js
を読み込み、FlutterEntrypointLoader
クラスで main.dart.js
をエントリーポイントとして読み込みます。ここまでは一緒です。flutter.js
の initialize については Customizing web app initialization | Flutter が参考になります。
async loadEntrypoint(options) {
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } =
options || {};
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
}
大きな違いは main.dart.js
の中身です。従来のビルド形式の場合は minified された JavaScript としてそのまま実行されていましたが、Wasm ビルドの場合は以下のコードとなっており、内部で .mjs
の import と .wasm
のコンパイルをしていることがわかります。
// 読みやすさのため、try-catchのエラーハンドリング部分を削っています
(async function () {
let dart2wasm_runtime;
let moduleInstance;
// Wasmをストリームして`WebAssembly.Module`へコンパイル
// ref. https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/compileStreaming
const dartModulePromise = WebAssembly.compileStreaming(fetch("main.dart.wasm"));
dart2wasm_runtime = await import('./main.dart.mjs');
moduleInstance = await dart2wasm_runtime.instantiate(dartModulePromise, {});
if (moduleInstance) {
await dart2wasm_runtime.invoke(moduleInstance);
}
})();
以上をまとめるとこちらの図のイメージです。
Firebase Hostingへデプロイ
Google I/O 2023 では、Firebase Hosting の Flutter Web Wasm 対応も同時に発表されました。
Today, we’re announcing official Firebase Hosting support for WebAssembly Flutter web, which includes new step-by-step instructions to help you get set up quickly.
https://firebase.blog/posts/2023/05/whats-new-at-google-io#support-for-flutter-web
Flutter Web の Wasm ビルドは、前述の通り現時点では build
しかサポートしていないためデバッグ実行では確認できません。そのため、Firebase Hosting にデプロイして動作確認します。
手順は 公式ドキュメント の通り進めれば、とくに難しいポイントも少ないので割愛します。躓いた場合や Firebase をそもそも使ったことがないという方は、以下の記事がキャプチャ付きで丁寧に解説されているので参考にしてみると良さそうです。
上記の記事にも記載されていますが、Firebase CLI でセットアップするだけで、裏でよしなに Firebase Hosting へのデプロイ権限を持つサービスアカウントを作成し、GitHub Actions のシークレットキーに登録してくれます。また、以下を利用した GitHub Actions のワークフローファイルまで作成してくれるので、ほぼセットアップ不要でデプロイ環境が整います。
Wasmホスティング時のHTTPリクエストヘッダーの指定
WebAssembly Mode のレンダラーである skwasm
では、マルチスレッドのために SharedArrayBuffer のセキュリティ要件を満たしている必要があります。
To take advantage of multiple threads, the web server must meet the SharedArrayBuffer security requirements.
SharedArrayBuffer のセキュリティ要件を満たすために、ホスティングされるサーバー側で以下のレスポンスヘッダーの指定が必要です。
Cross-Origin-Embedder-Policy: require-corp # credentiallessも可能だがrequire-corpの方がセキュア
Cross-Origin-Opener-Policy: same-origin
Firebase Hosting を利用する場合は、headers
を以下のように指定することで対応できます。
{
"target": "wasm",
"public": "build/web",
"headers": [
{
"source": "**/*",
"headers": [
{
"key": "Cross-Origin-Embedder-Policy",
"value": "require-corp"
},
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
}
]
}
]
},
もし、上記の COEP リクエストヘッダーの指定が漏れていたり、ブラウザが WasmGC に対応していない場合は Wasm モードでビルドしても skwasm
レンダラーで描画はされず、canvaskit
レンダラーにフォールバックされます。bool.fromEnvironment('dart.tool.dart2wasm')
のフラグを用いて Wasm がランタイムで処理されているかを判定でき、COEP ヘッダーがない場合はこの値が false
となることを確認しました。
複数サイトをホストする
やや脱線しますが、Firebase Hosting は 1 つのプロジェクトで複数のサイトをホスティングできます。
今回のサンプルリポジトリでは、ビルド方法による違いを比較するために 3 つのサイトそれぞれにデプロイしています。hosting:sites:create
コマンドを叩くか、あるいは GUI コンソールからポチポチしても作成できます。
# hostingするサイトを新規で作成
❯ firebase hosting:sites:create flutter-web-assembly-sandbox
こんな形で、ビルド形式によってホスティングする URL を 3 つ用意しました。
❯ firebase hosting:sites:list
Sites for project flutter-web-assembly-sandbox
┌───────────────────────────────────┬───────────────────────────────────────────────────┬─────────────────┐
│ Site ID │ Default URL │ App ID (if set) │
├───────────────────────────────────┼───────────────────────────────────────────────────┼─────────────────┤
│ flutter-web-assembly-sandbox │ https://flutter-web-assembly-sandbox.web.app │ -- │
├───────────────────────────────────┼───────────────────────────────────────────────────┼─────────────────┤
│ flutter-web-assembly-sandbox-html │ https://flutter-web-assembly-sandbox-html.web.app │ -- │
├───────────────────────────────────┼───────────────────────────────────────────────────┼─────────────────┤
│ flutter-web-assembly-sandbox-js │ https://flutter-web-assembly-sandbox-js.web.app │ -- │
└───────────────────────────────────┴───────────────────────────────────────────────────┴─────────────────┘
デプロイターゲットを設定する
作成したサイトの Site ID をそのまま使うのはやや大変なので、デプロイターゲットを設定して分かりやすい名前を付けます。コマンドが直感的に理解できるようになります。
以下の例では flutter-web-assembly-sandbox
という Site ID を wasm
というターゲットでデプロイできるようになります。
# サイトのデプロイターゲットを設定する
❯ firebase target:apply hosting wasm flutter-web-assembly-sandbox
✔ Applied hosting target wasm to flutter-web-assembly-sandbox
Updated: wasm (flutter-web-assembly-sandbox)
# これでデプロイできるようになる
❯ firebase deploy --only hosting:wasm
target:apply
コマンドを実行すると、.firebaserc
ファイルに以下の形式で追記されます。
.firebaserc
"hosting": {
"js": [
"flutter-web-assembly-sandbox-js"
],
"wasm": [
"flutter-web-assembly-sandbox"
],
"html": [
"flutter-web-assembly-sandbox-html"
]
}
最後に firebase.json
の hosting
を配列にし、さきほど作成したターゲットを指定すれば準備完了です。
firebase.json
"hosting": [
{
"target": "wasm",
"public": "build/web_wasm"
},
{
"target": "js",
"public": "build/web"
},
{
"target": "html",
"public": "build/web"
}
],
これで、以下コマンドでデプロイできるようになります。
firebase deploy --only hosting:wasm
最終的には、Melos を使ってビルドとデプロイをまとめて行うスクリプトにしました。
hosting:wasm:
run: |
flutter build web --wasm
cd ./firebase
firebase deploy --only hosting:wasm
以上を構成するリポジトリはこちらになります。
以下が、3つのビルド形式でホスティングした URL です。単なるカウンターアプリなので目立った違いはないですが、触って挙動をご確認いただけます。
BuildOptions | Hosting URL |
---|---|
--wasm |
https://flutter-web-assembly-sandbox.firebaseapp.com/#/ |
--web-renderer canvaskit |
https://flutter-web-assembly-sandbox-js.firebaseapp.com/#/ |
--web-renderer html |
https://flutter-web-assembly-sandbox-html.firebaseapp.com/#/ |
まとめ
今回は、Flutter Web の Wasm ビルドを中心に、Firebase Hosting へのデプロイや従来のビルド形式(CanvasKit, HTML レンダラー)と比較しながらその挙動を確認してきました。
iOS/Android/macOS などと比べて Web プラットフォームでの Flutter の利用は、やはりまだ物足りなさ(SEO や OGP なども)がありますが、Wasm の正式サポートで利用機会がさらに増えていきそうに思いました。
今回はブラウザでの実行を扱いましたが、Wasm の真骨頂はエッジサーバやコンテナなど汎用的な場面で高速な処理ができる、いわゆる Run Anywhere なケイパビリティを持っている点にあると思いますので、今後あらゆる場面でさらに一層 Flutter の活躍が見られそうだなと期待しています。
参考
- (8) Evolving Flutter's support for the web - YouTube
- WebAssembly の GC Proposal とは何か / どこに向かおうとしてるのか
- DartをWebAssemblyにコンパイルする(2023年3月版) - Qiita
- .mjs とはなんぞや?!
- Flutter, Dart, Wasm-GC - Wasm I/O Conference - March 23, 2023 - [shared publicly] - Google スライド
- (8) Flutter, Dart, and WASM-GC: A new model for Web applications by Kevin Moore @ Wasm I/O 2023 - YouTube
- [web] Flutter web using WebAssembly (WASM) · Issue #41062 · flutter/flutter
Discussion
こちら大変勉強になりました🙌
一点、現状の僕の環境(Flutter,Chrome最新)だと、firebase.jsonのwasmターゲットに以下のようにhttpヘッダーを追記する必要があるようだったので、今後他の方の参考になればと思いコメントさせてもらいます..!
<参考>
公式ドキュメント: ヘッダーについて:
コメントありがとうございます!
確認次第追記させていただきます🙇
ご指摘いただいた内容について手元で確認できましたので、セクションを追記させていただきました。
他、HTMLレンダラーが公式ドキュメントから削除されていたり、CanvasKitがDefaultモードに名称が変わっていたり本記事の内容も古い記載が多かったため、関連箇所の修正と注意書きを追記しました。
仕様変更にキャッチアップできてなかったため大変勉強になりました。
ご指摘ありがとうございました🙇♂️