Code

2016年6月18日土曜日

DOM は遅い?なぜ Virtual DOM を使うか

 先日 DOM は遅いですか?なぜ Virtual DOM を使うかと聞かれました。DOM は遅いというか、DOM 操作が遅いです。ブラウザーには reflow, repaint などの処理があります。最初 HTML と CSS を分析して、ノードツリーをレンダリングした後に、JavaScript でダイナミックにノードの高さなどを変更したり、さらにノードを追加したりすると、ツリー中の他のノードを影響されるし、ツリーを再構築するかもしれません。それは当然遅いです。
 実際 HTML のノードを遍歴する時や、ノードの属性を取得するときは遅いというか、むしろ速いです。これは jQuery 自分の event システムを持つ理由もなります。一つの要素をクリックすると、その event は buble して、親要素を遡って、ルートまで達して、event handler を呼び出します。もしこの中に DOM 操作がなければ、10 ms ぐらいで、できるかと思います。
 ただし、もしノードの offsetHeight などの computed style を取得して、さらにその値をベースに、ノードの style.height を変更するときに、ブラウザーは大変な仕事になります。
 通常ではブラウザーは変更をキャッシュして、ある時間、ある程度貯まったら、画面を一気に更新します。なぜなら、細かい更新をすると、main thread が止まったりするから、ユーザーには不親切です。が、offsetHeight などの属性を取得する時、ブラウザーはできるだけ正しい値を返すために、すべての更新を実行しなければなりません。次、style.height を変更すると、さらに、ツリーの中の他のノードの位置を計算します。この Read + Write が多くなると、main thread が応答しない場合もあります。
 じゃ、どのように DOM 操作を早くできるかというと、いくつかの方法があります。
 1 要素のスタイルを変更する場合は、width, height などを個別に変更するではなくて、新しい css class を作って、要素に入れると、こちらの更新が一つの reflow, repaint で済みます。
 2 また、offsetHeight などの値取得する時や、getBoundingClientRect() 関数を呼び出す時、できるだけ、Read を完了したら、Write を行います。つまり、Read と Write を分けます。
 3 DOM 操作を一つ一つするではなくて、まずキャッシュして、一気に行ったほうがいいです。例えば、documentFragment を使って、すべての要素を入れてから、その fragment を DOM に入れるとか。まず、要素の display を none にして(隠して)、その後に、height などを変更してから、display: block にして、再表示するとか。

 他には、
 4 アニメーションをするときに、opacityや、transform, scale など一つの layer になる css 属性を使いましょう。また、アニメーション対象の要素の position を absolute にするとか。

 Virtual DOM を使う理由は上記の 3 と関係しています。JavaScript でサイトを操作するとき、Virtual DOM を使うと、変更がメモリーにキャッシュされます。すべての操作が終わったら、Virtual DOM から DOM に更新しに行きます。この場合、ブラウザーはすべての変更を一つの reflow, repaint でできるため、速いです。
 今 React.js 以外、他に incremental dommithril などいろいろ JavaScript があります。まぁ、開発はコードを書くだけではなくて、テストなども考えなければなりませんので、今の時点で React.js は一番かなと思います。
 
 それでは。

2016年6月17日金曜日

2016年6月5日日曜日

Angular から React.js への移行

 昨日今やってるプロジェクトと似ている新しいウェブアップがリリースされました。AngularJS 1.0 を使っています。マネージャーたちは使ってみたら、なぜうちのプロジェクトと比べると、遅いと感じられるかと聞かれました。そもそも AngularJS 1.0 は重いからです。ぐるぐる回るロードアイコンが止まったり、なかなか応答が返ってこなかったりしたので、そう聞かれても仕方がありませんでした。
 じゃ、どうやって、その AngularJS プロジェクトを React.js に移行するかと考えました。
 まず、AngularJS 1.0 で ng-repeat を使ってるところを directive にして、React.js を使ってレンダリングしたほうがいいと思います。原因は ng-repeat は超を付くほど重いからです。key を使っても、ajax から返ってきたデータは毎回違うから、HTML の reuse がほぼ不可能です。逆に、React.js の場合、DOM ノードは Virtual DOM をベースに更新されるので、データが変わるなら、その分のみ更新されます。DOM ノードはそのままを使うケースが結構あります。
 具体的には、ng-repeat の Collection を prop として、Component に入れます。その中で、Rendering する。directive の作り方は:
.directive('react', () => {
  return {
    scope: { data: '=' },
    link: function (scope, element, attrs) {
      scope.$watch(data, onDataChange);
      function onDataChange () {
        React.render(
          React.createElement(REACT_COMPONENT, { data: data }),
          element[0]);
        };
     }
  }
});

 これで、data が変更するたびに、React.render が呼び出されます。Virtual DOM を使っていますので、複数 rendering しても、変更する部分だけ更新されます。

 次は ng-include や ng-view を使ってる部分を切り出して、React.js に個別にレンダリングします。ReactDOM.render 関数や、React.js 自身は同じページに複数の Component をレンダリング機能がサポートしています。ng-include などは独立な Component らしいので、controller の関数を React.js Component に入れて、データバインディングを完成すれば、すぐ使えます。
 基本的に、service や、factory 部分を Flux や Redux の Store に変わるので、そんなに変更はないはずです。ただ、$http みたいな機能は React.js 提供してないから、qwest など別のライブラリを使ったほうがいいでしょう。これを考えると $http の promise が resovle されたら、$rootScope.$applyAsync 関数が呼ばれますので、ページ全体の Watcher が2回呼ばれます。。。重いですね。

 最初の一歩は一番難しいですが、変更するたびに、どんどん新しい React.js Component もできるので、やりやすくなります。最近は React.js Develop tool を使って、他のサイトはどうやっているかが見れます。
  Chrome React.js Developer tool

 例えば Airbnb などは一つ一つ Component を作って、画面に入れ替わっていることが見れます。