ROCHAS

Chrome Developer Teamから学ぶサイトパフォーマンス

Frontrend x Chrome Tech Talk Night Extendedで、Addy氏、Jake氏、Paul氏のセッションに感動し、Frontrend Advent Calender 2013に参加させていただきました。9日目のRochasです。

昨今のモバイリズムの中、ユーザーの85%がデスクトップと同等かそれ以上にモバイルでの高速化を求め、57%以上がロードに3秒以上かかるサイトからは離脱してしまうとの統計結果が示すように、私達は様々なデバイスやアクセス環境でのテストが必要になりました。
これらの問題をより早い段階で効率良く解決していくにはどうしたらいいのでしょうか。

Addy氏はモバイルサイト開発のためのツールやワークフローについて。Jake氏はレンダリングパフォーマンスについて。 Paul氏はChrome Dev ToolsのRemote Debuggingをライブで披露してくださいました。
Chrome Developer Teamから学んだ中から特に心に残ったところをまとめてみたいと思います。

Agenda

  1. レンダリングプロセス
  2. Chrome Dev Toolsを使ったループ処理のデバッグ

  3. モバイルのタップの300msの遅延とuser-scalable=no

  4. グラフィクスパフォーマンス
  5. TranslateZ Hack
  6. まとめ

1. レンダリングプロセス

ページがロードされてから、ドキュメント内のタグを解析し、Webページに表示するまでをレンダリングといいます。
ブラウザで何が起きているのか、このレンダリングプロセスがパフォーマンスに大きく影響します。

  1. Parsing
    レンダリングエンジンがHTMLドキュメントのタグを解析し、DOMツリーが構築される。

  2. Recalculae Style
    DOMツリーとスタイル情報からセレクタマッチングが行われ、レンダーツリーが構築される。

  3. Layout
    レンダーツリーの位置的な情報やボックスサイズを元にスクリーンにLayoutされる。
    width, margin, border, left, top

  4. Paint
    レンダーツリーの視覚的な情報を元にスクリーンにPaintされる。
    box-shadow, border-radius, background, outline

Rendering Process

Chrome Dev ToolsのTimelineで検証してみると、ローディングが進む度に、Scriptが呼び出され、LayoutやPaintが繰り返し発生していることがよくわかります。ではこのTimelineを見て、何をどうチューニングすべきなのでしょうか。

Layout

  • Layoutは、DOMノードの追加・削除、スクロール、ウィンドウのリサイズ、オリエンテーションの変更など、位置的な情報が計算されることによって発生します。

  • Layout発生のトリガーとなるJavaScript
    offsetTop, offsetLeft, offsetWidth, offsetHeight
    clientTop, clientLeft, clientWidth, clientHeight
    scrollTop, scrollLeft, scrollWidth, scrollHeight
    scrollBy(), scrollTo(), scrollX, scrollY
    getComputedStyle(), getClientRect()
    height, width

  • Layoutは親要素に遡って再計算されるため、パフォーマンスに大きなダメージを与えます。
    position: relativeだとdocumentルートから再計算されます。どこでLayoutが発生しているのかを意識し、極力発生範囲を限定しましょう。

  • visiblity: hiddenを指定するとpaintが発生しますが、display: noneだと位置的な情報が失われるため、Layoutが発生してしまいます。

Paint

  • PaintはLayoutほどダメージは大きくないとはいえ、むしろ発生源が多く侮れない。box-shadowborder-radiusの併用はやめたり、値を10pxから5pxに下げるなどスタイルを調整してみましょう。

  • Paint負荷が高いプロパティ
    gradient
    box-shadow
    border-radius
    filter: blur
    background
    position:fixed
    background-attachment: fixed

  • CSSプロパティの中でもtransformopacityはCompositeされ、GUIレンダリングで処理できるためPaintタイムを抑えることができます。

  • Chrome Dev Tools > Setting > General > にはPaintを可視化する便利な機能があります。最近Canaryは挙動がかわり、chrome://flagsで設定するようになった模様。

    • Show paint rectangles ─ Paintの発生箇所を赤い線で表示。
    • Enable continues page repainting ─ ペイントタイム(ms)をヒストグラムで計測。
    • Show composite layer borders ─ Compositeされているレイヤーをオレンジの線で表示。

2. Chrome Dev Toolsを使ったループ処理のデバッグ

では実際にChrome Dev ToolsのCanaryで、Demoを使ってJake氏のデバッグを再現してみます。
緑色のボックスをリサイズすると、追随してテキストのwidthも変わる仕組になっています。
これをTimelineパネルから検証するとScriptingとLayoutが多発していることがわかります。

Timeline Panel

① スタートボタンから検証開始。
② グラフの突出している部分をドラッグで絞り込む。
③ 警告!の部分を選択すると詳細が表示されます。この例だと124行目がボトルネックとなっており、ドキュメント全体でLayoutが発生しているので事態は深刻です。
ここをClickするとSourceパネルの124行目に飛び、JavaScriptを修正することができます。
原因は、width=offsetWidthなループ処理。要素の位置を取得して、設定させるため、位置的な再計算が繰り返されているのです。

Source Panel

これを以下のように修正してみます。取得した値は何回も呼び出すのではなくローカル変数にキャッシュし呼び出しを1回にするとパフォーマンスは向上します。

Bad

while (i--) {
  var greenBlockWidth = sizer.offsetWidth;
  ps[i].style.width =  greenBlockWidth + 'px';
}

Better

var greenBlockWidth = sizer.offsetWidth;
  while (i--) {
    ps[i].style.width = greenBlockWidth + 'px';
  }

3. モバイルのタップの300msの遅延とuser-scalable=no

快適なユーザー体験を与えるためにはロードスピードだけではありません。ユーザーは何らかのアクションをしてからレスポンスがあるまで、100ms以上かかると遅いと感じるとされています。
しかしモバイルでは、タップしてからイベント発生までに300msの遅延が生じます。その理由はシングルタップなのかダブルタップなのかを判定するため300msのdelayが指定されているからです。

Click Eventではなくて Touch Events (touchstart/touchend) を使えばイベントの発生と実行が同期され、300msの遅延を防ぐことができます。しかしModern IEはTouch Eventsに対応していないため、Pointer Eventsを使わなければなりません。

Chrome for Android 32Firefox for Android 11ではviewportにuser-scalable=noまたはminimum-scale=1, maximum-scale=1で拡大禁止にすることで300msの遅延を防げるようになりました。

remove 300ms delayl

こちらはWebkit Bugzillaが行なったテストですが、依然iOS Safari、Androidは対応していませんし、Modern IEの場合は-ms-touch-action: noneといったように処理を分けなければなりません。
マルチデバイス対応に関してはFastClickなどpolyfillを使うという選択肢もありますが、ダブルタップやピンチによる拡大ができなくなる点は解決しません。
300msの遅延回避することで逆にアクセサビリティが損なわれてしまわないか、よく検討する必要があります。

4. グラフィクスパフォーマンス

setTimeout vs requestAnimationFrame

フレームレートとは動画やアニメーションで1秒間に何回フレームが描画されるかを単位FPS(Frame Per second)で表したものです。ブラウザの一般的なリフレッシュレートは60Hzであり、スムーズなアニメーションを実現するには16.67ms以内に処理を完結させる必要があります。

16ms=.0167秒 1/.0167秒=60FPS

従来のsetTimeoutsetIntervalの場合、フレームレートを一定に保つことが難しく、アニメーションがガタガタしやすい。 一方requestAnimationFrameは、60FPSを振り切らないよう考慮されており、またタブがアクティブでない場合は、実行回数が自動的に低下しCPUの負荷を抑えることができます。
JavaScriptベースのアニメーション、Canvas、WebGL、SVGで有効です。
ただしIE9以下、Androidは非対応なのでsetTimeoutによるフォールバックと、ベンダープリフィクスが必要です。

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();

CPU vs GPU

CPUレンダリングは1枚のスクリーン上で連続して処理を行うのに対し、 GPUレンダリングは描画処理を分割して並行処理を行うため負荷を分散させることができます。
このあたりの内部構造についてはginpei_jpさんが語ってくださっています。

現在Chromeでは以下の条件下ではほとんどのOSでハードウェアレンダリングモードになり、より高速なグラフィクスアニメーションが可能となります。
chrome://gpu/ から自分のOSでのハードウェアレンダリング対応が確認できます。

3D transform
Animation (transform, opacity, fillter)
Flash, Silverlight
Canvas
Video

またtransformopacityに関しては、Chrome、Firefox、Safari、Opera、(IE11は条件が違うかも)でハードウェアレンダリングが可能です。

requestFramaAnimation vs CSS transform

JSベースのアニメーションはrequestAnimationFrameでさえ、CPUレンダリングで処理されるため、負荷がかかってしまうケースがあります。
一方transformOpacityを伴うCSSアニメーションは、レイヤーが分割され(Composite)、GPUレンダリングで処理されるため、Paintの発生を押さえることができます。

scale → transform: scale(n)
move → transform translateX(npx)
rotate → transform: rotate(ndeg)
fade → opacity: 0..1

@keyframes top/left vs @keyframes transform

ただしCSSアニメーションが全て速いというのは早とちりで、top/leftを使ったキーフレームアニメーションではLayoutが発生し、ガタガタになってしまいます。 同じ@keyframesでもtransformを使うとスムーズになります。 Demoは-webkit-のみベンダープリフィクス付き。

違いがわかりにくいかもしれないが計測してみると一目瞭然。@keyframes transformはPaintタイムが0ms。オレンジの線のところでレイヤーがCompositeされ、GPUレンダリングになっています!

@keyframes top/left

@keyframes transform

GPU対応は今のところCSSアニメーションのほうが進んでいるけれども、JSにしかできない表現もあるし、canvas、WebGLやSVGなど、アニメーションを実装する方法は色々ありますので、特徴を抑えておきたいですね。

5. TranslateZ Hack

TranslateZ HackとはGPUレンダリングを利用したハックです。
-webkit-transform: translateZ(0);または-webkit-transform: translate3d(0,0,0);を指定すると、レイヤーがCompositeされ、Chrrome上でハードウェアレンダリングモードに切り替わり、パフォーマンスが改善されるのです。 JSベースや、CompositeされないCSSアニメーション(transformでない)に対して、半ば無理やりGPUレンダリングを実行しようという試みです。

TranslateZ Hackでスクロールパフォーマンスを改善する

ここでまたDemoを使ってPaul氏のデバッグを再現してみます。
body全体に背景画像を固定したposition: fixedなレイアウトは、Documentルートから画像が再配置されるため、LayoutとPaintの両方が発生します。 実際スクロールをするとバグる時があり、とくにモバイルだと事態は深刻です。これをTranslateZ Hackで改善してみます。

TranslateZ Hack

Bad

<style>
  body {
    background: url("images/bg.jpg") fixed;
    background-size: cover;
  }
  </style>
</head>
<body>

Better

<style>
  .bg {
      background: url("images/bg.jpg");
      position: fixed;
      -webkit-transform: translateZ(0);
      background-size: cover;
      top: 0;
      left: 0;
      z-index: -1;
      width: 100%;
      height: 100%;
  }
  </style>
</head>
<body>
     <div class="bg"></div>

ポイントは-webkit-transform: translateZ(0);をbodyではなく、あえて1つ下の階層のDivに指定している点です。 TranslateZ Hackは安物のビールのようなもの、やり過ぎはよくないのです!

Worst

body {
  webkit-transform: translateZ(0);
}

例えば、bodyや*アスタリスクだと全ての要素がレイヤーに分割されます。するとGPUの使い過ぎで逆にパフォーマンスは落ちてしまいます。
TranslateZ Hackはアニメーション要素だけに限定し、Chrome Dev Toolsで確認をしましょう。

6. まとめ

  • LayoutやPaintの多発はパフォーマンスにダメージを与える。
  • LayoutのトリガーとなるJavaScriptを探そう。
  • Paintの発生源をShow paint rectanglesやEnable continues page repaintingで探そう。
  • フレームレートはFPS meterで常に16.67msを維持しよう。
  • GPUレンダリングを使ってCPUメモリを解放しよう。
  • JSベースのアニメーションはsetTimeoutsetIntervalではなくrequestAnimationFrameを使おう。
  • CSSアニメーションは@keyframes top/leftより@keyframes transformがスムーズ。
  • TranslateZ Hackは用法用量を守りましょう。

ルールよりツール。─ Addy Osmani

Webはセマンティクス、リーダブル、アクセシブルであるべき。 ─ Jake Archibald

目指すところは、フレームレート60FPS以内、サーバーレスポンス200ms以内、ページインデックス1000ms以内。 ─ Paul Irish

Tipsは大事だけれども、継続的にTestを繰り返すことが最も大切。永久的なベストプラクティスなどなく、常に追い求める勇気と努力を惜しんではいけないということを学びました。
最後になりましたが、このイベントを支えてくださったみなさま、ありがとうございました。Google Teamとサイバーエージェントのスピーカーの方々がお互いリスペクトし合っているのがとても印象に残っています。参加できて本当によかったです。
それでは明日はtomofさん。 よろしくお願いいたします。

参考