モバイル上のJSフレームワークの実行可能性 – ReactとRedux

私が好むと好まざるとに関わらず、誰もが私のWebアプリをiOS9の搭載されたiPhone 6SやNexus 6Pで、超高速Wifiに接続して使っているわけではありません。

現実は甘くありません。3Gでの接続や、古いハードウェアも珍しくありません。Googleのレポートによれば、Androidのアクティブユーザは14億人だそうです。彼らの多くは間違いなく、最先端ではないハードウェアを使っていることでしょう。

AndroidのパフォーマンスについてのJeff Atwood氏の最近の記事などを読んだことがあるなら、モバイルWebには希望がないように感じるかもしれませんね。

その記事からいくつか注目すべき文を引用します。

端的に言えば、今日最も高速なAndroidデバイスとして知られているものでも、新しいiPhone 6Sよりも5倍遅く、2012年代のiPhone 5上のEmberに比べても少し遅いのです。しかも世の中にはそれよりずっと遅いAndroidデバイスが無数にあります。何と気がめいる話でしょう。

これが別にEmberに限った問題ではなく、私たちが十分リサーチをした結果、Angularや、他のもっと重く複雑なAndroid上のJavaScriptにも及ぶ話だと分かりました。でも一体なぜでしょうか?

“重くて複雑”というところが問題なのでしょうか?

まだ続きます。

Androidのエコシステムではこれが徐々にシステム的な問題になってきており、今後数年は解決しそうにありません。そしてこれがDiscourseの将来にも影響を及ぼしそうです。なぜかと言えば、私たちはJavaScriptがモバイル機器上でデスクトップ機に近いパフォーマンスを出すことに大きく賭けているからです。iOSでは明らかにこれが実現されてきていますが、Androidでは悲惨なほどに対照的です。

Atwood氏のモバイルWebエコシステムについての意見がありましたね。

彼はこう締めくくっています。

あと2年程度で状況が変わるだろうなどと楽観的なことはもう言えません。数えきれないほどの遅いAndroidデバイスが世の中に存在しているので、Discourseプロジェクトへの代替案を考え始めなくてはいけません。

がっかりですよね。もうこの話はやめましょうか。少なくともReact Nativeがあるので、実際にはネイティブアプリを開発しながらWebアプリをビルドしているフリをすればいいですよね。

アプリにとってモバイルWebはもう将来性がないのでしょうか?

ちょっと待ってください…

気づかなかったかもしれませんが、”Discourse”アプリとAtwood氏が言っていたのは、彼がDiscourseについての記事を投稿するのに使ったアプリのことです。これはフォーラムアプリのようなものですが、メタな話ですね。

とにかく、これに気づいた時、dev toolsのネットワークパネルを開いてDiscourseのページを見てみました。そこにあったのは659KB(gzipされたサイズです)の、有線接続で送信されたJSでした。

私に言わせれば、最初からモバイル上で使うのを放棄しているのと同じことです。

モバイル上で実行可能にするためにはもっといい方法があるはずです。

Atwood氏やDiscourseを責めたり、批判したりしているわけではありません。非常に多くのモバイルWebアプリが同じようにビルドされているので、このような問題はあらゆるサイトで起こり得ることなのです。

私の疑問は単純です。こういった重いツールやフレームワークは、モバイルデバイス上で実行可能なのでしょうか。

すべてが実行可能だとは思いませんが。

リサーチ結果を見てみましょう

Filament Groupのステキな皆さんが昨年の12月に研究結果を発表しました。5種類の広く普及しているWebフレームワーク上で、TodoMVCアプリの起動時間のパフォーマンスを調べたものです。明らかに、TodoMVCはやや人為的な例で、現実のアプリとは違うかもしれませんが、この手のリサーチにはうってつけです。一定のフレームワークについて、すべてのベースとなるアセットを備えており、少なくとも希望的にはベストプラクティスであるはずだからです。

あなたはこう思うかもしれません。「これは起動時間のパフォーマンスだろう? Atwood氏が言っていたのはランタイムのパフォーマンスのことじゃないか」と。

そうですね。Atwood氏はランタイムのパフォーマンスについて書いていました。これはまた後で触れますが、ともかく、ユーザにとっては何を待っていようが問題ではなく、起動時間も明らかにパフォーマンスの重要なパートなのです。

投稿全体も読む価値がありますが、このリサーチ結果のサマリーをグラフにまとめたものがあります。

load time screenshots
私の意見では、AngularとEmber(Atwood氏が言及した2つのオプション)のデータはモバイルの使用には完全に不適格です。

私がAmbersandの開発者の1人だと知っている人は、私が他のフレームワークを批判してAmbersandを売りつけようとしていると考えるかもしれません。

それは違います。

私がビルドした最新の2つのアプリには、Ampersandのコードはまったく入っていませんでした。私のReduxに夢中になっている旨の記事を読んでいたら驚かないと思いますが。

あなたが何を使おうと、私がユーザとしてそのアプリを使う時に受ける影響以外については、まったく気にしません。

ツールとは目的のための手段でしかないのです。

いえ、それはウソでした。もちろんあなたが何を選ぶかちょっと気にはなります。でもそれは例えばあなたが開発リーダーだとして、メガバイト単位のJSを送信するとアプリが遅くなるからという理由だけで、モバイルWebは実行可能ではないと結論づけてほしくはないからです。

ゲームオーバーではない

モバイルWebはもしかしたら十分高速なのかもしれません。デスクトップ機では感じない非効率性から、逃れられるフリをするのをやめるべきなのかもしれません。

もっと最初からミニマリストになるべきなのです。

才気あふれるDominic Tarr氏が以前こう言いました。

Dominic Tarr@dominictarr
高速なソフトウェアを書きたいなら遅いコンピュータを使うことだ。

アプリを開発する時、ローカルで携帯電話でテストしないWeb開発者が今日に至るまで、どのくらいいるでしょうか? ただ画面が小さいだけでなく、性能が低く遅いコンピュータ向けに開発しているのだと想定する必要があります。最終段階のリリース前チェックだけではなく、開発のワークフローの過程でモバイルを考慮に入れる必要があります。参考までに、私は開発の時にこのような手順で行っています

携帯電話の通信速度はどんどん速くなっていくかもしれません。でも高速なハードウェアによってこれらの問題が解決されると希望を持ち、現在のスピード問題を無視できると考えるのは近視眼的だと思います。もちろん、携帯電話の性能は向上するでしょうが、携帯電話だけがランタイムの目標ではありません。

モノのインターネット(Internet of Things)が…モノになってくると(おっと、この文は詩的にはイマイチですね #leavingitanyway)もっと他の携帯電話以外のプラットフォームで開発するニーズが出てくるでしょう。例えば腕時計、テレビ、VR、そして大画面ディスプレイに接続された小さなコンピュータなど。

いいぞ、Henrik。じゃあどうすればいいの?

まず、以前もこんな記事を書きましたが、URLだけから推測できるHTMLはすべてビルド時に静的なHTMLファイルにレンダリングされるべきです。ブラウザは、たとえ”遅い”モバイルのものでも、サーバから送信されたHTMLをレンダリングするのはかなり高速だということが分かったからです。

次に、私はもっと少ないもので多くのことをするべきだと思います。説明しますね。

Backbone、Ampersand、EmberそしてAngularにおける困難なことの大部分は、UIへ状態を適切にバインドしているかどうかに関係しています。もちろんそれ以上のことはしていますが、よく見てみれば、データの機能の多くでさえバインディングの機能をサポートすることに関連があるのです。結果として、それらは何らかのイベントシステムとともにリリースされ、ほとんどは監視可能な派生した/計算されたプロパティを作成する方法を備えています。

もちろんReactをこれらのビューレイヤとして使ってもいいのですが、Reactの大きな長所は、コストの安い再レンダリングをする際に、システムを複雑にする多くの機能の必要性を排除してくれることなのです。

シンプルな例を挙げます。モデルつきのBackbone.Collectionで、Backboneのパラダイムを使ってどのようにコレクション長をUIにレンダリングしますか? まずテンプレートの値を取得するのが確かに簡単な方法です。ただし、いつ最初にレンダリングされたかというポイントを知ってさえいればですが。どうやって値をバインドすれば、コレクションを変更した時、あなたがDOMにレンダリングした長さもアップデートしてくれるのでしょうか?

追加/削除/変更のイベントの全てで再レンダリングするようにセットアップすることができます。また、同じ動作をして、他のプロパティの動作も一緒に追跡する、監視可能なプロパティを作成することもできます。こうして、どんな変更があっても全て更新し、ビューにバインドすることができます。

ただしこれは全て、少しバカげた感じがします。Backbone.Collection のインスタンスを調べたことがあれば、 collection.models コレクションは単純にBackbone.Modelの配列であることはご存じでしょう。 初めからデータ長の値を含む配列、その名も.lengthが含まれています(ため息)。

これをReactなど、CPUにあまり負荷をかけずに、アプリ全体を好きな時に再レンダリングできるものと比べてみましょう(低負荷はReactの原則です)。そういうものが仮にあれば、レンダリングを担当する個々のコンポーネント内のrenderメソッドで直接、そういうlengthプロパティを使えばいい、ということになります。これで、アプリ内の任意のステートに変更が加わり次第、再レンダリングする処理が実現できます。

コードもこんなに単純な形になります。

React.createClass({
  render: function () {
      return <span>{this.props.species.length}</span>
  }
})

あるいは、React 0.14の新しいステートレス関数コンポーネントを使い、ES6の分割代入を利用し、speciesとして上記に示したオブジェクトの配列を使うと仮定すると、脚光を浴びる結果が得られるでしょう。コンポーネント全体のコードは、以下のようになります。

var Aquarium = ({species}) => (
  <span>{species.length}</span>
);

ここで美的な観点は脇に置いて、どれだけのものを減らさなければならないかを考えてみましょう。

  1. コレクションで、複数のイベントリスナーを登録する必要はなくなりました(コードの量とメモリの使用量を減らせます)。
  2. すでに.lengthに保持している長さの、監視可能な複製を作成するための新しいプロパティを定義する必要もなくなりました(コードとメモリを減らせます)。
  3. 何ということでしょう、こう考えると、モデルのコレクションを監視可能にする必要も全くありません。コレクションは、カスタムで作成された監視可能なモデルではなく、昔ながらの単純なJavaScriptオブジェクトを含む、昔ながらの単純なJavaScript配列となるでしょう(コード、メモリ、演算が大幅に減らせます)。

従来の重いフレームワークに備わっていた機能の多くは、もはや必要ありません。

そこでReactが、すごく受け入れがたいけれども他に選択の余地がない選択肢となります。手元でReactの簡単なテストを実行したところ、React-DOM 0.14をwebpackでビルドしてgzipすると、サイズは約37KBになりました。

比較としてjQuery 2.xは、min+gzipの組み合わせを使った場合29KBになります。従って、この方法はかなり有用です。私としてはjQueryと同様に、ゆくゆくはReactのコア機能の大部分が、ブラウザのAPIとして実装されることを期待しています。

とにかくもうそろそろ、監視可能なモデル、監視可能なコレクション、派生プロパティ上で変更を受信する機能とか何とかを求めるところから脱却して、以下のような動作のアーキテクチャに移行するべきでしょう。

  1. JSON風の構造を持つ単純なJS配列とオブジェクトから構成される、単純なアプリケーションのステートオブジェクト。
  2. ステートを変更する方法は全て、ミューテータ関数として表します。ステートは、ステートツリーの中で変更が必要になったときにいつでも変更を実行できます(ここで必要があれば、不変性の原則に従うこともできますが、その場合、変更が必要なものは全て置き換えていることを確認してください)。
  3. ミューテータ関数が実行されたら、アプリ全体をいつでも再レンダリングする。

少々単純過ぎる表現ですが、これがRedux本来の動作です。Reduxの概要は、Reduxに関する私のブログ投稿記事に詳しく書いたので、そちらを参照してください。Redux自体の実装を、上記のような単純なものにしようと努力する必要はありません。そうそう、比較のために申し上げておくと、Reduxのサイズは実質的には大したことはなく、約2KBです。

肝心なのは、ReactをDOMの同期に利用すると、捨てられるものが山ほどあるという点です。

コードがどれだけ減らせるか、オブジェクトと配列の単純なセットやスマートなレンダリングの呼び出しの処理のためにブラウザで実行しなければならない演算がどれだけ減らせるかを想像してください。

クラスシステム、イベントシステム、テンプレート作成システム、監視可能なデータ型のカスタムセット、jQuery風のDOMライブラリ(上述のものの中で必要)と、モデルとコレクションとそれらのプロパティを表す、アプリ固有の追加コード……これらすべてを送信することと相対します。モデルのインスタンス化と、機能を有効化するためのモデルが使用する内部ステートとキャッシュの保持には、より多くのメモリと演算が必要になります。changeをトリガーする必要があるかどうかを判断するための、値の比較(いわゆる「ダーティチェック」)にも、演算とメモリが必要です。

アプリの状態がほんの少し変わっただけで実行される必要があるコードが、何層も存在します。

自由に再レンダリングする機能を実現すれば、画期的に処理を軽くすることができます。

残念ながら私は、Atwood氏がリンクで提示している、Emberランタイムのパフォーマンステスト結果と突き合わせて比べられるような、完全なランタイムのデータを取っていませんが、上記の単純化によって必要な演算の回数が激減し、ランタイムパフォーマンスが大幅に向上するというのは、容易に想像できます。

また、誤解のないように説明しますが、読み込み時間とランタイムのパフォーマンスが良好であれば、ファイルサイズの合計にさほど神経をとがらせる必要はありません。

それで、これからどうするの?

モバイル端末上のWebブラウザは二流扱いを受けていますが、この現状に甘んじたくなければ、このパフォーマンスの問題に向き合わなければならないと、私は考えます。最先端ではないハードウェア上でも、Webの動作はもっと速くなるし、同時に開発者にも快適な体験を提供できると思います。ところで、開発者の体験についての私の話はあやしいぞと思われた方は、どうぞ Dan Abramovの「React Europe」でのプレゼンを見てください

もちろん、皆さんがお持ちの疑問に私が全て答えることはできませんし、この記事も、React+Reduxがある意味で万能のソリューションだと主張したくて書いたのではありません。ただ私は、この組み合わせに大きな可能性を感じています。

当面はここで述べたように、既知のHTMLをプリレンダリングすることで動作を軽くするアプローチが有効だと思います。私はこれからも、もっといろいろなものをビルドして、それについて考えたことを発信していきたいと思っています。皆さんもこんな活動に加わってくださるとうれしいです。

何かあれば、私のTwitterアカウントまでご連絡ください。 @HenrikJoreteg です。欲を言えば、皆さんがご自分のブログに、この記事に対する感想を書いてくださると、よりうれしいです。ガジェットWebをどんどん盛り上げていきましょう。

最後まで読んでくださってありがとうございました。以下もよろしくお願いします。