2014/07/16更新

[フロントエンド] ブラウザレンダリングの仕組みを理解して、ブラウザに優しいJavaScriptを書こう

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
ブラウザのレンダリングの仕組みはHTML5 RocksHow browsers workで詳しく解説されてきました。しかしそれらはとても詳細で、読破して理解するのは大変です。
今回のブログでは手軽にレンダリングの概要を理解できるように心がけました。またより詳しく学べるようなリンクも記載しました。
そしてブラウザのレンダリングの仕組みを理解した上で、どのようなJavaScriptを書くべきかについても記載しました。
画像


目次




ブラウザのレンダリングの仕組み

この章では、HTMLとCSSが読み込まれてから画面に表示されるまでの間に、ブラウザがどのような処理を行っているかを説明します。 ファイル読み込みから表示までの一連の流れは以下図の通りです。
画像
  • [1]
    読み込んだHTMLを解析してDOMツリーを生成します。解析方法の詳細は解析-概要 | HTML5Rocksをご参照ください。
  • [2]
    読み込んだCSSも解析してCSSの構造体を生成します。この構造体の呼び名はいくつかありますが、ここではCSSOM(CSS Object Model)と呼ぶこととします。CSSの解析の詳細はCSSの解析 | HTML5Rocksをご参照ください。
  • [3]
    DOMツリーとCSSOMから画面表示に必要なレンダーツリー(Render Tree)を構築します。レンダーツリーにはDOMの構造と装飾の両方の情報が含まれます。ここでレンダーツリーとDOMツリーの違いは何かと思うかもしれません。それは、DOMツリーには全てのDOMが格納されている一方で、レンダーツリーには表示する要素のみが格納される(headタグやdisplay:noneの要素は含まれない)ということです。この処理の詳細はレンダーツリーの構築 | HTML5 Rocksをご参照ください。
  • [4]
    レイアウト(またはリフロー)ではレンダーツリーが持つ各DOM要素の位置を決定します。「レイアウト」という呼び方はWebkit系で「リフロー」という呼び方はGekko系から来る呼び方です。この処理の詳細はレイアウト | HTML5Rocksをご参照ください。
  • [5]
    ペイントでは画面への描画処理を行います。この処理の結果ようやく画面に表示されます。この処理の詳細は描画 | HTML5Rocksをご参照ください。

以上がレンダリングの仕組みの概要です。 各項目においてそれぞれ細かな処理内容はありますが、ざっくりとした概要は上記の通りです。

画面の初期表示では上記の一連の処理が行われますが、JavaScriptによるDOM操作でもレンダリング処理の一部が実行されます。 JavaScriptからどのような処理を行うと、どのようなレンダリング処理が行われるのでしょうか。 次の章では具体的な例を用いて、その点を説明します。



どのような時にレンダリング処理が発生するのか

この章では、初期表示以外にどのような場面でレンダリング処理が発生するのかについて説明します。 この章を通して、どのような時にどんなレンダリング処理が発生するのかの勘所を掴んで頂けたら幸いです。

JavaScriptで要素のスタイルを変更する

JavaScriptから要素のスタイルを変更すると、変更した内容に応じてレイアウトやペイントが発生します。 以下の例では、bodyの各スタイルを変更した場合にどのようなレンダリングイベントが発生するかを示しています。
// 余白を変更する
document.body.style.padding = '30px'; // Layout と Paint が発生
// ボーダーを変更する
document.body.style.border = '10px solid red'; // Layout と Paint が発生
// フォント色を変更する
document.body.style.color = 'blue'; // paint のみ発生
// 背景色を変更する
document.body.style.backgroundColor = '#fad'; // paint のみ発生
上記の例の通り「レイアウトとペイントの両方」が発生する場合と「ペイントのみ」が発生する場合があります。 余白やボーダーを変更した場合には要素の位置調整が必要なためレイアウトイベントが発生し、その後画面に表示するためにペイントイベントも発生します。 フォント色などの色のみの変更で要素の位置を変える必要がない場合には、ペイントイベントのみ発生します。

JavaScriptからDOMを追加する

JavaScriptからDOMを追加することで、DOMツリー、レンダーツリー、レイアウト、ペイントの各イベントが発生します。
// ul以下にliを追加する
var ul = document.querySelector('ul');
var li = document.createElement('li');
li.textContent = 'JavaScriptで挿入したli要素';
ul.appendChild(li);

ユーザー操作

様々なユーザー操作でもレイアウトやペイントが発生します。以下にはそれらイベント発生するユーザーアクションの一例を示します。
  • スクロールする
  • ウインドウのサイズを変更する
  • :hover要素などにマウスオーバーしてスタイルが切り替わる
  • など



ブラウザは頭が良い

さて一つ前の章では、様々な場面でレンダリング処理が発生することが分かりました。 レンダリング処理は得てして重たい処理です。何度も頻繁に発生すると画面がカクつくことにもなってしまいます。

ブラウザはそのような自体を避けるために、レンダリング処理の回数を減らすための最適化を行います。 最適化の一つの手法として「何もしない」または「まとめて行う」というものです。
ブラウザはJavaScriptからのスタイルの変更要求をキューにためて最後にまとめて処理を行うことで、レイアウトとペイントの発生回数を最小限にとどめるようにします。 例えば以下のような関数があるとします。
function changePosition () {
    var icon = document.querySelector('.icon');
    icon.style.top  = '10px';
    icon.style.top  = '20px';
    icon.style.top  = '30px';
    icon.style.left = '100px';
}
この関数内では合計4回スタイルを変更していますが、レイアウトとペイントは最後に1回ずつしか発生しません。 このようにブラウザはレンダリングの回数を減らすように最適化を行います。



ブラウザのレンダリング最適化を壊さないJavaScript実装

しかしそのような最適化を壊すJavaScriptを書くことができます。 それはJavaScript内で要素の以下のようなスタイルを読み込む場合です。
  • offsetTop/Left/Width/Height
  • scrollTop/Left/Width/Height
  • getComputedStyle
これらのスタイルを参照(またはメソッド呼び出し)をJavaScriptから要求すると、ブラウザはその時点で最新のレンダリング結果を強制的に計算し、そしてその結果を返します。 つまり上記スタイルを参照することで、最適化のためにキューにためていたスタイル変更要求をこの時点で行ってしまうのです。

例えば以下のようにJavaScriptを書くと、ループの度にスタイルの計算が行われます。
var div = document.querySelector('div');
for (var i = 0; i < 100; i++) {
    // offsetLeftを参照するたびに、スタイルの再計算が強制的に行われる.
    var offsetLeft = div.offsetLeft;
    div.style.left = (offsetLeft + 1) + 'px';
}
このような実装は、せっかくのブラウザの最適化を台無しにしてしまいます。
このような事態を避ける方法は、要素位置の参照回数はできるだけ減らすことです。 例えば以下のように最初の1回のみ要素の位置を参照して、その後はキャッシュした情報を使うようにすることで、ブラウザの最適化を邪魔しないようになります。
var div = document.querySelector('div');
var offsetLeft = div.offsetLeft;
for (var i = 0; i < 100; i++) {
    offsetLeft += 1;
    div.style.left = offsetLeft + 'px';
}
このようにレンダリングの仕組みを理解することで、より高速なJavaScriptを実装することができるようになります。



最後に

今回はブラウザのレンダリングの仕組みと、それを意識したJavaScriptの実装についてブログを書きました。 レンダリングの最適化の話は60FPSを意識した実装などもっと多くのことを書きたいところです。その内容についてはまた別の記事で言及できたらと思います。

本ブログではフロントエンド技術を中心に発信しています。ぜひ気になったら、RSSTwitterをフォローしてみてください。最新の記事をお届けします☆
最後までご覧頂きましてありがとうございました。





こんな記事もいかがですか?

[取り組み] フロントエンドでコーディングスピードをアップさせる6つの方法!と思って書いてたら30個も書いちゃった。
[フロントエンド] フロントエンドの入社試験99問!難しいですよ〜w。
[フロントエンド] Webページを表示するテストの際に、通信速度を3Gに制限して表示してみよう
[フロントエンド] スマホ実機でのデバッグ手段を増やす!Macのプロキシを利用して、通信内容を確認する。
[フロントエンド] Chrome 35 Beta の変更点。Touch制御、新しいJavaScript機能、プレフィックスなしのShadowDOM
[フロントエンド]複数アカウントでのテストには、Chromeのユーザー管理を使って、Cookieを切り替えると便利
[フロントエンド] Chrome36βが出た。変更点など。element.animate、HTML Imports、Object.observe、他。
RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。