📘

Flutter前史: ChromeがFlutterになるまで

2022/07/09に公開
3

先日、とても面白い動画がYouTubeにアップされていました:
https://www.youtube.com/watch?v=xqGAC5QCYuQ

スライド:
https://docs.google.com/presentation/d/1Yhch7KkNC5wbkcsc9p5XkWouEha2hex65l6Pca_SqkM/edit#slide=id.g11cbab926eb_0_0

Flutterがどのように現在の形になったのか、Flutterと名前が付く前の歴史を、当時のFlutterの開発者であるEric Seidel氏Adam Barth氏が振り返った動画です。
これがとても面白く、前史を理解することで、Flutterが実はどのような位置づけにいるのか、Flutterが何であって何でないのか、よくわかる内容だったため記事にまとめたいと思います。

(筆者は英語がそこまで得意ではありません。解釈違いなどあればコメントで教えてください。また、分かりやすさのために沢山省略しています。ぜひ元動画も併せてみてください。)

全ての始まり: WebKitからBlinkがフォークされた

2013年4月3日、GoogleはChrome/Chromiumに使用するブラウザエンジンを、WebKitからフォークされたBlinkに変更しました。

WebKit プロジェクトと Chromium プロジェクトはここ数年複雑化の一途を辿ってきました。これにより、全体的なイノベーションの速度が低下してきたことは否めません。そこで本日、WebKit ベースの新しいオープンソース レンダリング エンジン Blink をご紹介します
https://developers-jp.googleblog.com/2013/04/chromium-blink.html

このプレス記事を執筆しているのが、動画にゲストとして登場しているAdam Barth氏でした。
Flutterの最も原始的な形は、Chromeのブラウザエンジンのフォークから始まった一つの実験でした。

実験的プロジェクト: Bravo

Webブラウザの無駄をそぎ落としたらどうなるだろう。URLからコードを取得し、ユニバーサルアプリとして動かす最低限なプラットフォームを作ってみよう、という発想で作られたプロジェクトがBravoでした。
URLからJavaScriptを読み込み、HTMLを介さずJSONによってシリアライズされたDOMを解釈し、OpenGLを直接叩くようなJavaScriptによるセルフホストなエンジンでした。

いくつかサンプルコードが公開されていました:
https://chromium.googlesource.com/experimental/chromium/src/+/wip/abarth/bravo/bravo/examples/test.bravo
https://chromium.googlesource.com/experimental/chromium/src/+/wip/abarth/bravo/bravo/examples/box_and_triangle.bravo

floatレイアウトは実装したくない、や、CoffeeScriptによってDOMのようなフレームワークを書いてみた、などの発言があり、時代を感じます。(CoffeeScriptについて、it was a horrible ideaと言ってます)

Webカスタマーとの対話

ブラウザエンジンBlinkがフォークされた後、カスタマーとの対話が増えたと語っています。そこで出てきた課題が、HTMLのパースをしている限りアニメーションのジャンクは避けられず、当時のブラウザで60fpsのアニメーションが難しいという点でした。(あくまで2013~2014年当時の話で、現在はかなり改善しているようです)
この解決のため、低優先度タスクのスケジューリングの改善再描画の改善など取り組んでいたようです。

2014年9月: Razor

Blinkをベースに、無駄を徹底的に排除したらどうなるだろう、という発想で作られたのがRazorです。
Blinkから大量のコードを削除していき、パフォーマンスの向上を図りました。
Element毎に分岐するパース処理やdocument.write、O(N^2)になるようなCSSセレクタ、floatやtable、段組みや縦書きなどの重たいWeb仕様の削除をすることで、HTMLパースが19倍、スタイルの再計算が3.7倍、ブロックレイアウトが2.9~12.5倍速くなったそうです。(これらの一部のWeb仕様、ここまでエンジンの足を引っ張ってるんですね…)

実際、これらの仕様を削除したからといってブラウザの表示が大きく崩れるわけではなく、実際の巨大なHTML5 Specのシングルページ版をベンチマークに使えていたそうです。

2014年10~11月: Sky

ここで、ついにFlutterの直接の前身であるSkyが登場します。
Flutterレポジトリの初期コミット: https://github.com/flutter/flutter/commit/00882d626a478a3ce391b736234a768b762c853a

エンジンとしては前述のRazorを使用しており、HTMLの代わりに.skyファイルが使われますが、見た目はほとんどHTML/CSSです。
言ってしまえば、爆速魔改造Chromeの上で動くHTML/CSS/JS = Skyといった感じです。

// https://github.com/flutter/flutter/commit/7da267c48bdc681817ed670fd0cc6108d37dc9b9
<sky>
<style>
square {
  margin: 50px;
  height: 100px;
  width: 100px;
  background-color: green;
}
</style>
<square></square>
<script>
var square = document.querySelector('square');
var timeBase = 0;

function animate(time) {
  if (!timeBase)
    timeBase = time;
  var delta = time - timeBase;
  var rotation = Math.floor(delta / 10);
  square.style.transform = 'rotate(' + rotation + 'deg)';
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
</script>
</sky>

2014年12月: Sky Framework

Polymerを参考に、Web Component的にUIコンポーネントを定義しています。

// https://github.com/flutter/flutter/commit/be756e732950978364b240ea3aafd53a88f53e98
<sky>
<import src="/sky/framework/sky-element/sky-element.sky"/>
<import src="/sky/framework/sky-button/sky-button.sky"/>
<import src="/sky/framework/sky-box/sky-box.sky"/>
<import src="/sky/framework/sky-checkbox/sky-checkbox.sky"/>
<import src="/sky/framework/sky-radio/sky-radio.sky"/>
<sky-element name="widget-root">
<template>
  <style>
  div { display: paragraph; }
  </style>

  <sky-box title='Buttons'>
    <sky-button id='button'>Button</sky-button>
    <div>highlight: {{ myButton.highlight }}</div>
  </sky-box>

  <sky-box title='Checkboxes'>
    <div><sky-checkbox id='checkbox' />Checkbox</div>
    <div>highlight: {{ myCheckbox.highlight }}</div>
    <div>checked: {{ myCheckbox.checked }}</div>

    <div><sky-checkbox id='checkbox' checked='true'/>Checkbox, default checked.</div>
  </sky-box>

  <sky-box title='Radios'>
    <sky-box title='Group One'>
      <div><sky-radio group='foo'/>one</div>
      <div><sky-radio group='foo' selected='true' />two</div>
      <div><sky-radio group='foo'/>three</div>
    </sky-box>
    <sky-box title='Group Two'>
      <div><sky-radio group='bar'/>A</div>
      <div><sky-radio group='bar'/>B</div>
      <div><sky-radio group='bar' selected='true' />C</div>
    </sky-box>
  </sky-box>

</template>
<script>
module.exports = class extends SkyElement {
  attached() {
    this.myButton = this.shadowRoot.getElementById('button');
    this.myCheckbox = this.shadowRoot.getElementById('checkbox');
  }
}.register();
</script>
</sky-element>

<widget-root />
</sky>

またこの頃、Androidで動作するSkyShell.apk(URLからSkyアプリを読み込み実行するアプリ)が作成され、実機でSkyのパフォーマンステストなどが行えるようになりました。
このSkyShellには、現在のhot reloadの原型である「振ってリロード」が搭載されていたそうです。開発者はコードを変更したら、端末を振ることでURLからリロードさせていたと語っています。

2015年2月: 脱JavaScript

JavaScriptはWebのエコシステムで育っている言語のため、とにかく大勢のユーザがすでにいます。
Adam氏は沢山の要望をv8に対して出したそうですが、一つも修正が入ることは無かったそうです。

例として挙げられてるIssueは7年ほど経った今もOpenでした:
https://bugs.chromium.org/p/v8/issues/detail?id=3624

ここで様々な別の言語を試していくうちに、Dartに出会いました。
JavaScriptとは対照的に、出したIssueは高速に修正されこのチームとコラボレーションしたいと決まったそうです。

一瞬で解決されるIssue:
https://github.com/dart-lang/sdk/issues/22457
https://github.com/dart-lang/sdk/issues/23598

DartはもともとAlt JSとして開発されており、当時はGoogle内部のGoogle Adsなどの重要で複雑なWebアプリで使われていました。
Dartのこの身軽さのおかげで、Flutterの成長とともにDartはUIフレームワーク向きの言語という素晴らしい性質を持つようになりました。

Dartが導入されたコミット:
https://github.com/flutter/engine/commit/464f03c914e618e8850f57b45dee475ce65bf164

2015年3月: Effen

ここで、Polymerに造詣の深いRafael Weinstein氏がReactにインスパイアされたReactive FrameworkであるEffen(fn)を作ります。(同僚のAaron Boodman氏がReactに熱中しており、影響を受けたそうです)
これが今日のFlutterフレームワークに育っていきます。

// https://github.com/flutter/flutter/commit/202f99d71d6ea77eba7bd0c11ee085b9397daed2

class MyComp extends Component {
  MyComp({
    Object key,
    sky.EventListener onClick // delegated handler
  }) : super(key: key);
  
  Node render() {
    return new Container(
      onClick: onClick,
      onScrollStart: _handleScroll // direct handler
    );
  }
  _handleScroll(sky.Event e) {
    setState(() {
      // update the scroll position
    });
  }
}

現在のWidgetと似ていますが、違いもいくつかあります。
WidgetではなくComponentである点や、buildではなくrenderである点、BuildContextが存在しない点、戻り値がNodeである点などです。
Containerは既にこの時点であり、HTML Elementに対応しているため、HTMLでできることは大体できる強い存在だったようです。(この性質は現在も受け継がれていますね。現在のContainerにも沢山の機能があります)

2015年5月: さようならDOM, ようこそCanvas API

今までは、ブラウザのDOMやRender Treeを用いて描画が行われていましたが、ここでついに自前の描画システムを導入します。
Effenの一部としてDartによるRenderNodeを作り、これによってRender Treeを構築するようになりました。(Effen2, fn2)
今までHTML Elementによって実現されていたContainerコンポーネントは代わりにRenderNodeを持つように変更されました。

実装は、HTML Specの大著者であることで有名なIan Hickson(Hixie)氏で、Adam氏は「HTML Specってとっても手続き的な文章だよね、Dartという手続き型言語があるんだけどこれでHTML Spec書いてみない?」と提案したところ「いいアイデアだね!」と言って一度もコンパイルせずに大量のコードを書いてきたそうです

// https://github.com/flutter/engine/commit/0a09212fe591becdb56426953da28e2fecda03ca
import 'dart:math';
import 'dart:sky';
import 'package:sky/framework/layout2.dart';

class RenderBlueCircle extends RenderBox {
  void paint(RenderNodeDisplayList canvas) {
    double radius = min(width, height) * 0.45;
    Paint paint = new Paint()..setARGB(255, 0, 255, 255);
    canvas.drawCircle(width / 2, height / 2, radius, paint);
  }
}

void main() {
  RenderView renderView = new RenderView(root: new RenderBlueCircle());
  renderView.layout(newWidth: view.width, newHeight: view.height);
  renderView.paintFrame();
}

大分見覚えがありますが、後のRenderObjectに繋がります

2015年6月: スケールアップ

Effen2をベースに、マテリアルUIやNavigator、AssetBundleなどアプリの作成に必要な部品が実装されました。
Widgetという概念もこの時点で誕生しています:
https://github.com/flutter/engine/commit/350f16c93ef6b3a4b2a0187ae08fae036b94380f

2015年7月: Effen3(fn3)

ここで、Effen2はとても重大な課題にぶち当たります。
当時、今でいうところのWidgetとStateは同じ1つのオブジェクトで管理されていました。
ですが、ユーザがあまり理解していないと、同じオブジェクトを複数個ツリーに挿入できてしまうため、同じStateを共有してしまいバグります。
これを解決するため、WidgetとStateを別に分けるリファクタを行い、Effen2はEffen3になりました。
WidgetはStateを生成するファクトリーのため、複数回ツリーに挿入されても、別々のStateが生成されバグが解消します。

// https://github.com/flutter/engine/commit/d0e31391827de4f3314616e04854e484b145cb59
Prototype of fn3

This patch contains a prototype of a new widget framework. In this framework,
Components can be reused in the tree as many times as the author desires. Also,
StatefulComponent is split into two pieces, a ComponentConfiguration and a
ComponentState. The ComponentConfiguration is created by the author and can be
reused as many times as desired. When mounted into the tree, the
ComponentConfiguration creates a ComponentState to hold the state for the
component. The state remains in the tree and cannot be reused.

2015年10月: Effen3がFlutterになる

Effen3の開発が終わると同時にリネームされ、現在のFlutterになりました。

https://github.com/flutter/engine/commit/02f45a5d06d5a49cd86ddaca68b530c9cd14dc59

改めて見ると、短い開発期間でものすごい勢いでイテレーションしていったことが分かります。(1年ほどでほぼ現在の形になっている)
チームメンバーも当初は6名ほどと言っていましたし、強いメンバーが集中してコードを書いていた様子です。

まとめ

いかがだったでしょうか、Flutterが実はChromeを基に開発を重ねた結果生まれたものという事実は、なかなか意外だったのではないでしょうか?
実際、Engineの設計を解説する記事では、FlutterとChromeの設計に近い部分があると説明されています。
Chromeの設計が根底にあると思えば、納得のいく事実なのではと思います。

以下、三行まとめです:

  • モバイルで60FPSで動作するモダンなユニバーサルアプリを作れないか、というモチベーション
  • Chrome(Blink)をどんどんリファクタしていった
  • Reactを取り込んで、今日のFlutterになった

Discussion

mjhdmjhd

補足です。
記事を読んで面白かった方は、こちらの動画も開発の経緯などが説明されていてオススメです。
http://www.cs.cmu.edu/~bam/uicourse/830spring20/05-830-2020-03-23-Lecture-10-Flutter.mp4

こちらでは "Sky" project, an ever smaller subset of Chrome と表現されています。Chromeのサブセットですね。
Flutter Origins

動画中程では、Can we make a fast (60+fps), beautiful applications for mobile devices using Chrome? ということで、モバイルで60FPS出るようなパフォーマンスの良いChromeが作れないか?という問いから生まれたと説明されています。
Why not the Web platform?

もしもブラウザの後方互換性、HTMLパースやDOM、JavaScriptを消し去れたらどうだろう、という「異世界転生ブラウザ」プロジェクトがFlutterなんですね。

Cat-sushiCat-sushi

異世界転生ブラウザ、言い得て妙ですね。
使わせていただきます。