RSS

ReFlowの原因とマークアップの最適化

by LINE Engineer on 2011.11.4


自己紹介

ネイバージャパンのUIT(User Interface Technology)チームの裵完理(ベワニ)です。

概要

CSSやJavaScriptを使って複雑なデザインや動的なページを実装しているサービスが増えてきていますが、速度低下などの問題が発生しやすくなっています。これを100%直すことは難しいですが、改善するにはブラウザレンダリングプロセスを理解する必要がありますので、理解した上で改善方法を探してみましょう。

ブラウザレンダリングプロセスの理解

ブラウザの基本構造

ブラウザの基本構成図

  • User Interface – アドレスバー、戻る・進むボタン、ブックマークメニューなど、メインウィンドウに表示(document)されるページ以外の部分
  • Browser Engine – UIとレンダリングエンジン間のアクションを制御するもの
  • Rendering Engine – リクエストしたコンテンツを表示させるもの。例えばコンテンツがHTMLの場合、HTMLとCSSをパースして表示させる。
  • Networking – HTTPリクエストのようなネットワーク通信機能を担当
  • UI Backend – コンボボックスやウィンドウなどの基本的なUI要素。プラットフォーム非依存のインタフェースを提供する。背後ではOSのUIメソッドを使っている。(XPのセレクトボックスとWIN7のセレクトボックスが異なっていることを考えると理解しやすい)
  • JavaScript Interpreter – JavaScript をパースし実行する(chromeのV8など)
  • Data Storage – 永続的なレイヤー。クッキーなど, ある種のデータをハードディスクに保存する。HTML5ではWeb Databaseなどの完全なDBがブラウザで提供される。

レンダリングエンジンの流れ

レンダリングエンジンはネットワークレイヤーからリクエストしたコンテンツを受け取ります。その後、通常は次のようなフローを経ます。

画面構成が完了された後に動的な変化が発生したら?

ブラウザはある変化が発生したら最低限の対応をするように設計されています。もし、あるエレメントのcolorの属性が変更されたらそのエレメントに限ってrepaintが発生します。しかし、エレメントのポジションに変更が発生した場合そのエレメントにrepaintはもちろんエレメントが属しているレイアウトまで伝搬されてしまいます(Reflow)。htmlエレメントのフォントサイズを大きくするなどの大きな変化は全体のレンダーツリーのrepaintとreflowを発生してしまいます。

Reflow? Repaint?

Repaint(or Redraw)

エレメントのスキンに変化が現れるが、レイアウトには影響がない場合発生します。(visibility, outline, background-colorなど) Operaによると(*1)repaintが発生した瞬間、ドキュメントのDOM Treeの他のノードまでスキャンしないといけなくなるのでコストが高いそうです。

Reflow

ドキュメント内のノードのレイアウトとポジションを再計算してから表示することになるので、repaintよりももっとパフォーマンス低下を発生させるプロセスです。あるエレメントに対してReflowが発生したらこのエレメントにはすぐReflow Stateが立ち、親・子エレメントはもちろんその親のエレメントまでレイアウト計算を行います。結局はページ全体を再び描画することとほぼ同じです。

” Reflows are very expensive in terms of performance, and is one of the main causes of slow DOM scripts, especially on devices with low processing power, such as phones.In many cases, they are equivalent to laying out the entire page again.”
Reflowはパフォーマンス低下を発生させるプロセスであり、性能が低いデバイスでは既に遅いDOMスクリプトをもっと遅くする原因になります。多くのケースでReflowはページ全体を再びレイアウトする結果を生みます。

何がReflowを起こすのか?

あるエレメントにスタイルの変化が発生したとしましょう。この変化は子要素には何の影響もないとしてもブラウザはチェックしないと分かりません。従って小さな変化でも子要素はもちろんページ全体にReflowが発生してしまいます。Mozillaでは(*2)以下のケースでReflowが発生すると書いています。

  • Window resize
  • フォントの変化
  • スタイル追加又は削除
  • 内容変化(inputボックスにテキスト入力など)
  • :hoverのようなCSS Pseudo Class
  • クラス属性の動的な変化
  • JavaScriptを使ったDOMの変化
  • エレメントのoffsetWidth/offsetHeight(画面上での座標)を計算するとき
  • スタイル属性の動的変化

Reflowの影響を避ける、又は少なくする

クラスの切り替えでスタイルを変更する場合はできるだけDOM構造上、最後にあるノードに与える

クラス切り替えを使うとReflowを完全に避けることはできませんが、その影響を少なくすることはできます。できるだけDOM Treeの深いノードにクラスの変化を起こすとreflowの影響範囲を全ページではなく一部のノードに縮めることができます。ということで全ページを囲んでいるwrapperにクラス変化を起こすのは避けるべきです。またOOCSSを使うとあるエレメントにたくさんのクラスを適用していますが、実際にはreflowの影響範囲を最小化しパフォーマンスが向上します。

インラインスタイルはできるだけ使わない

DOMはとても遅い構造体です。さらにインラインスタイルが与えられている場合はreflowはページ全体にかけて何回も発生してしまいます。インラインスタイルを使わず、クラスと外部スタイルのみならreflowは一回だけ発生することになります。

アニメーションが適用されたエレメントはできればposition:fixed又はposition:absoluteで指定

一般的にJavaScript,CSS3でwidth/heightまたは位置を変更するアニメーションは秒単位でreflowを発生します。このようなケースでエレメントのposition属性をfixed又はabsoluteに指定すれば他の要素のレイアウトに影響を与えないのでページ全体のreflowの代わりにエレメントのrepaintが発生することになります。こうすることでコストを減らす効果を得られます。

クオリティとパフォーマンスの間で妥協する

一回1px動くアニメーションのAと一回3px動くアニメーションのBがあるとしましょう。この時は両方アニメーションの計算とReflowが同時多発しCPUの使用率が高くなります。でもAがBよりコストが高いです。スペックがいいデバイスでは両方同じく見えると思いますが、スペックが低いデバイスではその差をすぐ感じられます。

テーブルレイアウトは避けよう

テーブルでレイアウトされたページはプログレッシブページレンダリングが適用されません。さらにすべてロードされた後から計算し画面に表示することになります。Mozillaによると(*3)テーブルレイアウトではほんの少しの変更があってもテーブル全体の全ノードに対してReflowが発生すると述べています。またYUI data tableウィゼットの開発者のJenny Donnellyによるとレイアウト用じゃないデータ用でも当テーブルにtable-layout:fixed属性を与えるのがデフォルト値のautoより性能が良いと述べています。

IEの場合、CSS Expressionを使わない

このCSS Expressionのコストが高い理由はドキュメント又はドキュメントの一部がReflowされる度にExpressionが実行されるからです。もしアニメーションのところでReflowが発生したら場合によっては数千、数万回のExpresstionが実行されることになります。というわけでCSS Expressionは避けるべきです。

JavaScriptでスタイルを変更する場合、できるだけ一回で済むようにする

ある要素にスタイル変更をする場合

var toChange = document.getElementById('elem');
toChange.style.background = '#333';
toChange.style.color = '#fff';
toChange.style.border = '1px solid #ccc';

このように実装すると重なったReflowとRepaintが発生してしまいます。
この場合は以下のように実装すると発生数を減らせます。

#elem { border:1px solid #000; color:#000; background:#ddd; }
.highlight { border-color:#00f; color:#fff; background:#333; }
document.getElementById('elem').className = 'highlight';
CSSの下位セレクタは少なくするほど良い

ここではCSS Recalculationについて話してみます。CSSのRuleマッチングプロセスではRuleが少なくなるほどコストが減ります。例えば.LySubというクラスがLyContentsの中で唯一の要素だとしましょう。以下の二つの例を見てみましょう。

/* 例1 */
.LyWrap .LyContents li .LyBox .LySub {display:block;width:250px;}

/* 例2 */
.LyWrap .LyContents .LySub {display:block;width:250px;}

例1のように使うのはコードの可読性を高めるためだと思います。メンテナンスのためには可読性ももちろん重要なところですが、例1のように5段階にかけてRuleを書くとパフォーマンス低下の恐れがあります。さらにこのようなCSSコードが5~10行ではなく500~1000行になる場合は相当のパフォーマンス低下をもたらします。というわけで例2のように必要なRuleだけを使う必要があります。もし可読性が気になるならコメントに書くのが効果的です。

position:relativeを使うときには注意しよう

ページを新しく開いたりReflowが発生してCSS Calculationが行われる場合、Box model calculation→Normal flowの順で計算が行われます。一般的にエレメントはmargin, border, padding, content(width,height)などのBox modelを計算してからNormal flow状態のレイアウトに配置されます。

  1. Box model calculation
    各々エレメントのMetrics計算を先に行います。
  2. Normal flowで線形に配置
    Box model calculationの後、マークアップの順に従って画面に配置を行います。(ただ、position:absolute又はfixedの場合はNormal flowを飛ばしてPositioning過程に入ります。)
  3. Normal flowの後
    FloatかPositionかによってPositioningの過程が決まります。ケース毎のシナリオは次のようです。

    • ケース1:Float属性を持つ要素
      Normal flowの後Positioning過程はなく右もしくは左の行けるところまで移動します。
      (Box model → Normal flow → Floating)
    • ケース2:position:relative;と一緒にtop,leftなどの位置の値を持つ要素
      Normal flowの後Positioning過程を行います。
      (Box model → Normal flow → Positioning)
    • ケース3:position:absolute, fixedを持つ要素
      Box modelを計算した後Normal flowの過程を飛ばしPositioning過程で位置されます。
      (Box model → Positioning)

position:relativeがposition:absolute又はfloatよりコストが高いのが分かります。UL,OLのようなリストで繰り返して使うLI要素にposition:relativeとleft,topの属性を指定する場合パフォーマンス低下をもたらします。

参考資料