FlutterをWebで動かす「Hummingbird」の仕組みについて調べた

先日開催されたFlutter LiveでFlutter for Webであるところの「Hummingbird」が発表されました。

Hummingbirdについてどういったメカニズムで動くのか気になったので調べていたところ、下記に詳細な解説がありました。

この記事の二番煎じではあるのだけど、↑の原文をざっくりと読んでみてだいたいわかってきたので自分なりに雑に要約してまとめておきます。

Flutter on Webを実現するために必要になること

  • Dartをweb用にコンパイルすること
  • 既存のFlutterの資産のうちWeb用のFlutterに実装する機能を選ぶこと
  • 実装されるべきWebの資産(HTML+CSS/SVG/Canvas/WebGL)を選ぶこと

Flutter on Webのプロトタイプとして考えたこと

  • 案1) コアレイヤーのウィジェットだけ自前で提供する(レイアウト計算とかその辺はWebに任せる)
  • 案2) 案1)に加えてレイアウト計算の部分も自前で提供する(レンダリングオブジェクトを直接HTMLエレメントにマッピングする)
  • 案3) dart:ui(要はFlutterのDartで書かれたエンジン以外のほぼ全て)をブラウザで動くようにする

色々試した結果、案3)がベストだという判断になった。案3)を実現するためにFlutter Web Engineを自前で作った。

このエンジンはフレーム単位でWidgetのビルド->レイアウト計算->描画を行うというもの(ゲームとかと一緒)。

Flutterのポート作業

大きく分けて、

  • Widgetのビルド
  • レイアウト計算
  • 描画

の作業がある。

Widgetのビルドに関してはそんなに問題はなくてスムーズにできた。

一方レイアウトについても概ね問題なかったが、テキストのレイアウトに関しては少しトリッキーな方法を使ってる。

というのもテキストのレイアウトに必要な最大/最小のwidthやheightやbaselineのサイズなどの情報を得る方法がWebでは簡単にはいかないから。

これを解決するために、まずテキストをHTMLのDOMとして作成させて、そのDOMに対してoffsetWidthやgetBoundingClientRectを呼ぶことでパラメーターの取得を行っている。

最後に描画についてだが、これが一番厄介で今でも試行錯誤しているところ。

描画といえば最終的にHTML/CSS, Canvas, SVG, and WebGLあたりでアウトプットすることになるわけだが、WebGLへの描画については動作が今ひとつ保証しきれない部分があり却下。全てのRenderObjectからHTMLエレメントを生成する案に関しては激しいブラウザのAPI変更にいちいち対応していくのがしんどすぎるだろうということがわかってきた。

よって今は「HTML+CSS+Canvas」と「CSS Paint API」を利用した描画を採用している。

HTML+CSS+Canvasについて

基本構造はFlutterで生成された要素をHTML+CSSで表現できるものはHTML+CSS(通称DomCanvas)で、それ以外の要素はCanvas 2D(通称BitmapCanvas)を使って描画するようにしている。

ちょっと頑張ったところとしてはCanvasの描画パフォーマンスを効率化するところ。

普通にデフォルトの2D Canvasを使って各Widget要素をそれぞれ描画すると非効率なので、独自実装のBitmapCanvasというのを作って、その上で2D Canvasを動かすようにした。

scene,opacity,transform,offsetのようなFlutterでいうところのlayerクラス(Flutterのレンダリングツリーのベース部分)はそれぞれ普通のHTMLエレメントとCSSのopacity属性やtransform属性を利用して構築している。

このようにして基本的には普通のHTML+CSS+CanvasでFlutterの描画構造を実装できた。

よってモダンな全てのブラウザ動くようになっている。

CSS Paint APIについて

まだChromeOperaにしか実装されてないがCSS Paint APIを使うとCanvasへの描画と同様で、しかもJSのメインプロセスと別に動いてくれる(a paint worklet)のでパフォーマンスがよくなる。

Flutter for WebではこのCSS Paint APIを実験的に利用している。

このAPIは独自のCSSプロパティにシリアライズされていて、paint workletはこのプロパティを読んで実行するという感じになる。

現状はJSONを使ってシリアライズを行っているが、typed arrayを使えるようになる(すでにHoudiniではサポートされてる)とメインプロセスから切り離して実行できるのでもっと効率よくできる余地がある。

その他

  • Flutter WebではMaterial Widgetのすべてを利用でき、the Flutter Galleryのほとんどがwebで動くようになっている
  • FlutterのWebアプリはDart製のライブラリをすべて利用可能
  • FlutterのWebアプリはJS interop(DartからJavascriptAPIを使うときに便利なやつ)のパッケージをすべて利用可能
  • FlutterのWebアプリはCSSを使うことができない
  • FlutterのWebアプリはプラットフォーム独自の機能(ARKitとかローカルファイルを直接いじったりとか)をすることはできない
  • 既存のFlutterアプリをそのままWeb用に移行はできない。が、iframeやshadow Domを使うことで将来的に可能にしていきたい
  • React ComponentなどFlutter以外のコンポーネントの埋め込みには対応してない。が、将来的にできるようにしてみたい(すでにPlatformViewを使うとiOS/Androidのネイティブビューを埋め込めるので似たようなことができるか否かみたいな感じ)。
  • ソースコードはもうすぐオープンソースとして公開する

おわり

ソースコードが公開されないことにはより詳細なことがわからないのでなんともいえないですが、React Native for Webとはまた違った方向性の実装だと思うので今後も注目していきたいです。