こんにちは。中原です。
CacooではGoogle+ Hangouts、Google Drive、Google Apps、ChromeウェブストアなどたくさんのGoogleの技術を利用しています。そこで先日2013/5/15~5/17にサンフランシスコで開かれたイベントGoogle I/O 2013に参加して来ました。
イベント中は本当にたくさんのセッションがあったのですが、その中から2つのセッションの内容をみなさんにも紹介したいと思います。我々のようにWebアプリケーションの開発をしている人にはとても興味深い内容になっています。
この2つのセッションはWebページの描画のパフォーマンスに関してのセッションになっています。
「描画」といわれてもピンときませんが、以下のような場合に発生する画面を更新する処理のことです。
- ページを開いたとき
- JavascriptやCSSで要素を移動、変更したとき
- ウインドウよりも大きいサイトをスクロールしたとき
ここでは自分がページを見ている時に描画にどのくらいの時間が掛かっているかを調査する方法と、改善するためのヒントを紹介したいと思います。
かなり長い記事となっていますが、ぜひ最後までお付き合いください!
注意
説明にはGoogle Chromeを使用しているので、それ以外のブラウザでも全く同じとはいかないかもしれません。ただ多くのブラウザで共通する内容もあるかと思います。
またこれから説明する内容の一部(とくにGPU関係のCSS)は将来Webブラウザの進化によってまったく意味のないテクニックになっているかもしれません。あらかじめご了承ください。
準備
ここで紹介している一部のツールは、2013年5月現在の安定版Google Chromeには含まれていません。内容を試される場合はChrome Canaryを利用してください。
またMacやLinuxを利用されている方は、「chrome://flags」を開いて「すべてのページでGPU合成を行う」を「有効」に変更してお試しください。
もし最新のハイスペックPCを使っているのであれば、ちょっと古いロースペックのPCで試してみてください。より違いがわかるかもしれません。
それではみなさんも一緒に試しながら記事を読んでみましょう!
目指すパフォーマンス
どの程度の間隔で画面が更新されれば、スムーズと言えるのでしょうか?
現在、多くのディスプレイ(スマートフォン含む)は1秒間に50~60回画面を更新しています。実際に私のPCでは「画面のリフレッシュレート」が60ヘルツとなっています。
1000msec / 60Hz = 16.6msec
つまり1回の画面更新(1フレーム)を16.6msecで処理できれば非常にスムーズに見えることになります。
60フレーム/秒、16.6msec/フレーム これが目標になります。
ただ、この目標はかなり高い目標になっています。このあと測定方法を紹介しますが、すべてのシーンで16.6msecを達成するのは非常に難しいです。時間がかかっても、ユーザが気付かない程度に抑えるというという目標が現実的かもしれません。
1秒間に何フレームでているか
では実際に1秒間に何フレームでているか見てみましょう。準備はできていますか?
Chromeの「ディベロッパー ツール」(以下、DevTools)を開いてください。これを読んでいるみなさんなら、もちろんショートカットを知っていますよね ( Ctrl+Shift+I / Comamnd+Option+I )。
右下の歯車をクリックしてください。
「Show FPS meter」をチェックします。すると右上にFPS(1秒あたりのフレーム数)が表示されます。
この状態で色々なサイトを開いて画面をスクロールさせてみてください。リアルタイムでFPSの表示が変わっていきます。
なんの処理が行われているか詳しく見る
今度はDevToolsのTimelineで見てみましょう。
DevToolsのTimelineタブを開いてください。
左下の●をクリックすると記録が始まります。
この状態でまた画面をスクロールさせてください。もう一度●をクリックすると記録が終了します。
ではFramesをクリックしてみましょう。
各フレームに掛かった時間、そこで行われた処理の内容を見ることができます。
バーのグレーの部分がなんの処理か気になりますが、Googleの解説によると下記のような時間だそうです。
- DevToolsで計測されない動作時間
- ディスプレイの更新サイクルのアイドル時間
Paint(描画)処理を見てみてください。「900x70」のように描画の範囲が書いてあって、更に青いエリア枠でブラウザのどの部分が描画されたのか確認することができます。
画像がある場合は画像のデコード・リサイズ処理が目立ちます。
描画の負荷を減らすにはこの描画範囲を小さくすること、描画時間を短くすること、無駄な描画をなくすことが重要になってきます。次はこの描画が発生している部分をもっと見やすくしたいと思います。
Paint(描画)発生箇所を見る
Timelineからでも描画の発生箇所は確認できますが、もっとわかりやすく見てみましょう。右下の歯車をクリックしてください。
「Show paint rectangles」をチェックします。
この状態で画面をスクロールしたり、リンクにマウスを載せたりしてみてください。赤い四角が表示されます。この赤い部分が描画が行われてる部分です。色んなサイトを見てみましょう。ほとんど赤くならないサイトもあれば、スクロールするたびに全体が赤くなるサイトもあって面白いですね。
ここでGoogle I/Oのセッションで紹介されたデモページを見てもらいたいと思います。
デモ - その1
スライドのこのページにあるDEMOを開いてみてください。
「Add flowed elements」をクリックすると四角が追加されるとその部分で描画が発生するので赤く表示されます。
何度かクリックして追加される四角が見えなくなってもページ全体の高さが変わるため、ブラウザのスクロールバーが再描画されているのがわかります。
今度は「Toggle right padding」をクリックしてみてください。ぱっと見何も変化はないのに真ん中のエリアが大きく赤くなっています。実はボタンを押すたびに真ん中のエリアにCSSの「padding-right:10px」が付加されたり削除されたりしています。
見た目上は何も変化がない場合も、再描画は発生するということですね。
デモ - その2
次に、スライドのこのページにある右下のDEMOを開いてみてください。
たくさんの画像が縦に並んでいます。画面をスクロールせずにそのまま見てみましょう。
しばらくの間、スクロールバーが赤く点滅し続けます。これは画像が読み込まれ、ページ全体の高さが変わり、スクロールバーの表示が更新されているからです。
次に画像にマウスが乗らないようにマウスのホイールでスクロールさせてみてください。特に赤い変化はないと思います。
今度は画像にマウスが乗るようにしてマウスのホイールでスクロールさせてください。今度は画像の部分が赤くなったと思います。画像の部分にはhoverで透明度(opacity)の変更と影(box-shadow)を変更し、「-webkit-transition」で1秒間のアニメーションで変化させています。
みなさんもhoverで表示を変えたりすることは多いと思いますが、画面をスクロールすることでhoverが大量に発生し、大量の再描画の原因になることがあるので気を付けましょう。
ここで右の「Hover effects during scroll」のチェックを外してから、画像の上にマウスを置いてスクロールさせてみてください。今度はあまり赤くならないかと思います。このチェックをはずすとscrollイベントが発生した時に200msec間はhoverの処理がされないようになり、再描画の発生しにくくなっています。
無駄なPaint(描画)を避けるには
postition:fixed に気をつける
「postition:fixed」を使ったサイトを見てみてください。スクロールした時にfixedの要素で毎フレーム再描画が発生しています。
これはスクロールするたびにページ全体の中でfixedの要素の位置を変更する必要があるからです。
fixedの要素が非常に大きく、その要素の描画の負荷が高い場合はその影響が非常に大きくなります。
overflow:scroll に気をつける
overflow:scrollを指定して中をスクロールした場合、再描画が発生します
無駄なイベントに気をつける
前のデモでも紹介しましたが、hover、スクロール、マウスオーバー、マウスホイール、タッチムーブのようなイベントはマウスなどを動かした場合に連続で発生してしまいます。このイベントの中で表示を変更するような処理を行うとその動作中は再描画が発生し続けることになります。
負荷が高くなる場合は前の対策のように表示を変更する減らすことはできないか検討したほうが良いかもしれません。
また、表示を更新しなくても、Javascriptでイベントリスナを登録しているだけで要素を動かしている間は毎フレームJSの処理時間が発生することになります。必要がない場合はイベントリスナ自体を登録しないことで負荷を減らすことができます。
長いPaint(描画)処理の原因
再描画の処理はないに越したことはありませんが、ユーザの利便性を考えると「position:fixed」など必要な場合もあります。ただ再描画もすぐに処理が終われば特に問題もないはずです。問題があるのは非常に描画に時間が掛かるような要素が連続で再描画されるような場合です。
具体的には次のような要素になります。
- 見た目複雑なCSS
- 画像のデコード・リサイズ
Androidでも見てみる
これまではPC上のChromeで動作を確認して来ましたが、AndroidのChromeでもDevToolsは利用できます。モバイル環境はPCと比べるとCPUのスピートやメモリなどのリソースが厳しいので描画の影響がより大きく現れてきます。
利用するにはAndroidとPCをUSBケーブルで繋ぐ必要があります。PC側にはAndroid SDKをインストールしておく必要があります。
具体的には手順はこちらをご確認ください。PC上のChromeから先ほどのTimelineを見ることができます。
「Show paint rectangles」をチェックすればAndroid上でも赤い再描画のエリアを端末上で確認することができます。
GPUを使った高速化
描画の負荷を減らすのに、GPUを使うテクニックがあります。「CSSでアニメーションさせるとGPUアクセラレーションが効いて滑らかになる」といった言葉を聞いたことがある人も多いと思います。ここからはChromeでGPUがどのように使われているかを紹介していきます。
ただし、Webブラウザはどんどん進化しています。あなたがこれを読んでいる時点では全く役に立たないテクニックになっている可能性あるので注意してください。
HTMLが画面に表示されるまで
1. HTML、CSS、Javascript処理
HTML、CSS、Javascriptが処理されDOMツリー、レンダーツリーを構築します。
2. Paint(描画)処理
ブラウザの内部で大きなビットマップにテキスト、画像などを描いていきます。ホーダー(丸角含む)、ドロップシャドウなども描いていきます。この処理はCPUで行われます。前に赤く表示されていたのはこのビットマップの描画している部分になります。
このビットマップはページ全体を含んでいます。つまり最初にブラウザの見えている部分だけでなく、スクロールしないと見えない部分も含めて描画処理が行われてます。
3. ビットマップをGPUに転送
前のステップで処理されたビットマップをGPUに転送します。GPUではビットマップは小さなタイルという単位に分割されます。各タイルはテクスチャとしてGPUメモリにキャッシュされます。
ブラウザ上でページをスクロールしたとき、再描画も発生せずその部分のデータがGPUキャッシュにあればCPUはビットマップを送信する必要はなく負荷がかかりません。
4. ディスプレイに出力
GPUがディスプレイにデータを出力します。
CPUでアニメーション
GPUの機能を使わずにアニメーションする場合、どのような処理になるのか紹介したいと思います。
右から左へ画像が動くとき、移動前と後の2箇所で再描画が発生します。そしてGPUメモリでその描画エリアが含まれるタイルを更新する必要があります。
GPUでアニメーション
次にGPUの機能を使用してアニメーションする場合を紹介します。
GPUを使う場合、2つの要素は2つのGPUレイヤーに分けられます。そしてGPUは各レイヤーの位置情報を持っています。ちなみに各レイヤーはそれぞれのタイルを持っています。
右から左へ画像を動かすにはGPUレイヤーの位置を変えるだけです。再描画は必要ありません。
そしてGPUが複数のGPUレイヤーを合成します。このようにCPUの描画処理は発生せず高速なGPUだけで処理が行われます。
GPUレイヤーでは以下のような操作が可能です。
- レイヤーの移動
- レイヤーの拡大・縮小
- レイヤーの回転
- レイヤーの透明度の変更
これらの操作を頻繁に行う要素がある場合、GPUレイヤーを分けることで再描画を行わずに描画の負荷を下げられるかもしれません。
GPUレイヤーの生成
以下の要素はページを読み込んだときに自動的にGPUレイヤー生成されます。
- Canvas
- Video
- Plugin
- iframe
ただしこれはプラットフォームやデバイスなどの環境によって異なる場合があります。
頻繁に表示が更新されるものは、最初からGPUレイヤーを別にして描画の負荷を減らしているようですね。
手動でGPUレイヤーを作成こともできます。次のような3D Transform関連のCSS属性を持っている場合、その要素がGPUレイヤーになります。
transform(-webkit-transform)
- translateZ
- scaleZ
- rotateX
- rotateY
- rotateZ
- translate3d
- scale3d
- rotate3d
backface-visibility
- hidden
2D TransformはGPUレイヤーを生成しません。
GPUレイヤーを見てみる - その1
では実際にGPUレイヤーを確認してみましょう。DevToolsを開いて右下の歯車をクリックしてください。「Show paint rectangles」と「Show composited layer borders」をチェックしてください。
青い四角が表示されると思います。この四角が先ほど説明したタイルのサイズになります。GPUレイヤーになっている要素はオレンジ色の枠で囲われます。枠線が非常に細いのでよく見てくださいね。ここではdiv要素に「-webkit-transform : translateZ(0)」のCSSを指定しています。
前に紹介したHoverのデモを見てみてください。右側のチェックボックスと上のヘッダ部分がオレンジの線で囲われていてGPUレイヤーになっています。
前に「position:fixed」が描画処理の原因になると書きましたが、この2箇所は「position:fixed」を指定しているのに描画処理が発生していません。それは、2つの要素がGPUレイヤーになっていたからなのです。
右の「position:fixed elements promoted to layers」のチェックを外してみてください。GPUレイヤーを作らないようになります。この状態で画面をスクロールしてみてください。fixedの要素でも描画処理が発生するようになり赤い部分が非常に大きくなります。
次に、スライドのこのページにあるDemoを開いてみてください。
「Use position: left」をチェックして左上の四角をクリックしてみてください。これはボックスの「style.left」を変更し「-webkit-transition」でアニメーションを行っています。グレーのボックス部分が赤くなっていると思います。
今度は「Use translateX」をチェックして左上の四角をクリックしてみてください。今度は「style.webkitTransform」を「translateX(~)」に変更して「-webkit-transition」でアニメーションを行っています。グレーのボックスはアニメーションの最初と最後だけ赤くなります。そしてアニメーション中はボックスにオレンジの枠線がついています。
アニメーションの最初にGPUレイヤーが作成されボックスはそのレイヤーに移動します。GPUレイヤーになっているので再描画は発生していません。アニメーションが終わるとGPUレイヤーを削除しボックスをもとのレイヤーに描画しています。
今度は「Use translate3D (prepromote)」をチェックしてください。この時点でボックスにオレンジ色の枠がついてGPUレイヤーになっていることがわかります。左上の四角をクリックするとさっきと異なり、アニメーションの最初と最後も赤くなりません。ここでは「style.webkitTransform」を「translate3D(~,0,0)」に変更して「-webkit-transition」でアニメーションを行っています。
メモ: スレッド合成
上記のページでPeriodic JS jank」をチェックしてもう一度3つを試してみてください。このチェックをすると、定期的に長いJavascriptの処理が実行されるようになります。DevToolsのTimelineで確認してみてください。
おそらく「Use position: left」の場合はどの環境でもカクカクなると思いますが、残りの2つは環境によってカクカクならない場合があります。カクカクならない場合、おそらくChromeで「スレッド合成」が有効になっています。これが有効になっていると、Javascriptの処理に時間が掛かっていても別のスレッドでGPUの処理を進め、表示がスムーズになります。「スレッド合成」の機能は「chrome://flags」で変更できるので違いをぜひお試しください。
GPUレイヤーを見てみる - その2
GPUレイヤーを確認する方法をもう一つ紹介します。「chrome://tracing」を開いて、「Record」ボタンをクリックしてください。
「cc」、「devtools」、「disabled-by-default-cc.debug」だけをチェックして「Record」ボタンをクリックしてください。
別タブで前に紹介したページのようにGPUレイヤーのアニメーションを実行してください。そのあと元のタブに戻って「Stop tracing」をクリックしてください。
○をクリックすると下の部分にGPUレイヤーの位置、大きさ、重なりを見ることができます。GPUメモリの使用量も見ることができます。
左右のカーソルキーで前後のフレームに移動するので、カーソルキーを押したままにするとアニメーションによってGPUフレームの位置がどう変わっているか確認することができます。
全部GPUレイヤーにする?
「GPUレイヤーを使うと速いなら全部の要素をGPUレイヤーにしちゃえ!」と思った人、やめてください。
各GPUレイヤーはタイル単位の大きさでGPUメモリを必要とします。大量のGPUレイヤーは大量のメモリ(VRAM)を必要とします。メモリは有限なので足りなくなるとキャッシュの古いものから再利用していきます。
大量のGPUレイヤーが作られた結果、キャッシュが有効に使えなくなり、キャッシュにないデータをCPUに送ってもらう必要がでてきます。結果、処理が遅くなるということになります。またGPUレイヤーの数が増えるとレイヤーの並び順のソートの処理も大きくなってきます。
まとめ
長々とお付き合いくださいましてありがとうございました。
パフォーマンスチューニングということで、まずはボトルネックを調べる。
- 描画が頻発しているの場所、原因はどこか
- 長い描画が発生している場所、原因はどこか
そしてボトルネックを解消する。
- 描画のエリアを減らす
- 描画の時間を減らす
- 描画の発生を減らす
- 場合によってはGPUレイヤーの利用を検討する
といったところでしょうか。
スマートフォンやタブレットが増えてきてチューニングが必要な場合も出てくるかと思います。そのときの参考にしていだければと思います。
我々もこの内容をこれからの開発に役立てていきたいと思います。
0 Comments