Edited at

仮想DOMに思いを馳せていたらそもそもブラウザのレンダリングがわかってなかった

More than 3 years have passed since last update.

仮想DOMとか、react.js とか最近聞くようになりました。フロントエンドに関わるものとして、UIパフォーマンス向上を志すなら、少し触って明日の糧にしようと思っていろいろ調べていたら、そもそもブラウザのレンダリングとは何かもよくわかっていなかったので出直そうと思った、というごくつまらない話です。


ブラウザのレンダリングとは

DOMの読み込み、リサイズ、イベント発火で再描画、くらいふわっとわかったフリになっていた。


  1. DOMツリー

  2. レンダーツリー(DOM ツリーのビジュアルな側面を表します。だそうです)

  3. レイアウト(Mozillaではリフローと呼ぶらしい。ややこしいから統一しろ)

  4. ペイント

がブラウザが行うレンダリングの順番。


リフロー、ペイントが発生するトリガー


  • DOM ノードの追加、削除、更新

  • display: none (リフローとペイント)、あるいは visibility: hidden (位置の変更は起きないので、ペイントのみ) による DOM ノードの見た目の変更

  • ページ中の DOM ノードの位置の移動やアニメーション

  • スタイル属性のちょっとした変更のためのスタイルシート追加

  • ウィンドウサイズの変更やフォントサイズの変更、そしてスクロール など、ユーザーの操作

ユーザー操作が行われる度に、ブラウザ側のリフロー・ペイントは常時行われる(ex. スクロール、ウィンドウリサイズなど)。jsによるスタイルの変更では物によるけど、色の変更ならペイント、サイズの変更も伴うならリフロー・ペイントが発動している。DOMの追加や複製、削除では1~4までもちろん全て行う。


ブラウザのレンダリング最適化

参照URLの文中にあるけど、jsのDOM操作で同期的に呼び出されるスタイルの上書き、例えば

$('div#hoge').css('margin-left', 30)

.css('margin-left', 50)
.css('margin-left', 0)
.css('margin-left', 16);

こんなよく分からないコードがあるとして、ブラウザは賢いので、最後の行しかリフロー・ペイントを行わない。レンダリングの回数を減らすように最適化しているんだそうです。ブラウザ超賢い。


ブラウザの最適化を狂わせるメソッド


  • offsetTop、offsetLeft、offsetWidth、offsetHeight

  • scrollTop、scrollLeft、scrollWidth、scrollHeight

  • clientTop、clientLeft、clientWidth、clientHeight

  • getComputedStyle()

この人が激昂されている通りなんですが、


これらのスタイルを参照(またはメソッド呼び出し)をJavaScriptから要求すると、ブラウザはその時点で最新のレンダリング結果を強制的に計算し、そしてその結果を返します。 つまり上記スタイルを参照することで、最適化のためにキューにためていたスタイル変更要求をこの時点で行ってしまうのです。


悪い例として

var target = $('#fuga');

for (var i = 0; i < 100; i++) {
var offsetLeft = target.offsetLeft;
target.css('margin-left', offsetLeft + 1);
}

ループ内で使用している、offsetLeftメソッドで都度リフロー、スタイルの再計算が強制的に行われることになる。塩梅が良くないので、初回ロード時のみ要素位置を参照し数値としてキャッシュして参照させるってことになる。コードにしたら別になんてことはない。

var target = $('#fuga');

var offsetLeft = target.offsetLeft;
for (var i = 0; i < 100; i++) {
offsetLeft += 1;
target.css('margin-left', offsetLeft);
}

ただし、動的にビューがリフロー・リペイントを繰り返しそうなスマートフォンの場合はそうもいかないと思う。


イベントのデータを何らかの変数にキャッシュして、描画ロジックでそれを参照する


この言葉に尽きる。描画ロジックの度に要素.scrollTop()してたらブラウザのレンダリングに負荷がかかるというのが、ここに来てようやく腹落ちした。


今すぐにも出来そうなブラウザへの優しさ


例えば d:n はDOMに差し込む前にセットしておく


bad.js

$('<div id="foobar"></div>').append('body').hide();


これではDOMツリー、レンダーツリー、リフロー、ペイント起きてますよね? 起きてるんですよ。


soso.js

$('<div id="foobar" style="display:none;"></div>').hide().append('body');


もしくは、要素をd:nするようなクラスを付与


good.js

$('<div id="foobar" class="element-is_not-display"></div>').append('body');


することで、予め読まれたCSSOMの情報を汲んだ上でブラウザはレンダーツリーingするわけですし、描画に負荷をかけないわけです。


というわけで、本日は以上です。

http://www.slideshare.net/hayatomizuno/ss-23379553

このスライドも少しだけ詳しいです。