60 (フレーム・パー・) セカンズ:Pinterestの描画パフォーマンス最適化のケーススタディ

今日はWebサイトやWebアプリの描画パフォーマンスをどう改善したらいいのか、ということについて取り上げようと思います。私たちWeb開発者にとって、この分野は比較的新しく注目し始めたエリアで、ユーザエンゲージメントとユーザ体験に影響があるため重要です。

フレームレートはWebにも影響がある

フレームレートとは、連続したイメージを端末がスクリーンに映し出すレートのことです。秒間フレーム数(FPS)が低ければ肉眼で個別のフレームを判別でき、FPSの数値が高ければユーザにとっては反応が速いと感じられます。ゲーム業界ではおなじみの概念となっているものですが、Webにも当てはまるのです。

長時間に及ぶイメージのデコード、不必要なイメージのリサイズ、重いアニメーションとデータ処理はすべてフレーム落ちを起こす可能性があり、結果としてフレームレートは下がり、jankが多いページになってしまいます。後ほど”jank”が何を意味するのか、簡単にご説明しましょう。

なぜフレームレートが重要なのか?

フレームレートが高くスムーズならユーザエンゲージメントは促進され、あなたのWebサイトに訪れたり、アプリを使ったりするユーザ数にも影響を及ぼします。

今年2月のEdgeConfでFacebookが言及して認めたところでは、A/Bテスト時にスクロールのフレームレートが60FPSから30FPSへと下降し、エンゲージメントが激減したのです。そうは言っても、高いフレームレートを達成するのが難しく、60FPSは無理だと言うなら、せめて何とか動きをスムーズにしたいですよね。もしアニメーションを自分で作っているなら、requestAnimationFrameを使うひとつのメリットは、ブラウザが動的に調整してフレームレートを通常値に保持してくれることです。

スクロールに関して不安があるケースでも、ブラウザがフレームレートを管理してくれます。しかし、大量のjankを起こしてしまっている時は、それほどいい仕事はしてくれないでしょう。ですから、長時間の描画やJavaScriptの長時間の実行、または長くかかるもの全て、すなわち大きな遅延を避けるようにしてください

推測するな、テストしろ!

始める前に、一歩戻って私たちの手法を確認しましょう。みんなWebサイトやアプリの動作を速くしたいですよね。実際、私たちは、正しいだけではなく速く動作するコードを書くことに対して給与をもらっているのです。私たちは締め切りを抱えた忙しい開発者なので、つい、見聞きしたアドバイスに安易に頼ってしまいがちです。しかしそうした時、問題が起こります。ブラウザの内部の改変は非常に早く、今日遅いものが明日速くなるかもしれないからです。

覚えておいて欲しいのは、あなたのアプリやWebサイトは作成者に特有なもので、何を構築しているかによってパフォーマンスの問題も異なるということです。ゲームを最適化するのと、ユーザが200時間以上も起動しているアプリを最適化するのではまったく違います。ゲームなら、メインループに集中して、フレームごとに稼働するコードを最適化すればいいのです。DOMを多用するアプリケーションでは、パフォーマンスの最も大きなボトルネックはメモリの使用かもしれません。あなたにとってベストな方法は、あなたのアプリを計測する方法を学び、コードの動作についてよく理解することです。そうすればブラウザに変更がかかっても、自分にとって何が問題となるかが明確なので、あなたも、あなたのチームも、情報を基に判断を下すことができます。ですから、どんな場合でも、推測するな、テストしろ! ということです。

フレームレートと描画パフォーマンスを計測する方法について後で話しますから、待っていてくださいね。

注:この記事で参照しているツールには、about:flagsで”Developer Tools experiments”を有効にしたChrome Camaryが必要です。(私たち、Addy OsmaniとPaul LewisはChromeのデベロッパリレーションズのエンジニアです。)

ケーススタディ:Pinterest

先日、私たちはPinterestで、ポニーのボードに追加するためのポニーを探しました(Addyはポニーが大好き)。それで、私たちはPinterestのフィードに行き、追加できるポニーを探して、スクロールしていました。

Screen Shot 2013-03-25 at 14.30.57-500
Addyが自分のPinterestボードにポニーを追加しているところ。拡大画像

Jankはユーザ体験に影響する

スクロールしてみて最初に気づいたのは、このページ上でのスクロール速度はあまり良くないということです。上下にスクロールするのには忍耐が必要で、非常に遅く感じられました。こういう事態に出くわすとユーザは不満を持ち、そのうちサイトを訪れなくなってしまいます。もちろん、ユーザに去って欲しくはないですよね。

Screen Shot 2013-03-25 at 14.31.27-500
Pinterestではユーザがスクロールする際にパフォーマンスのボトルネックがある。拡大画像

この、安定したフレームレートが破綻してしまうことを、Chromeチームでは”jank”と呼んでいます。そして、ここでの原因は分かりませんでした。スクロールする時、フレームが描かれているのに気づきますよね。これをとにかく視覚化してみましょう。Frameモードで起動して、どのような遅延なのかを見てみましょう。

注:望ましいのは、安定した高いFPSで、理想的にはスクリーンのリフレッシュレートと合致しているものです。多くの場合、60FPSを目指すのがベストですが、これは保証されているわけではありませんから、対象とする端末をチェックしてみてください。

JavaScriptの開発者の最初の直感として、私たちはまずメモリリークを疑います。たぶんガベージコレクションの後にオブジェクトが残ってしまっているのかもしれません。でも、実際のところ最近の事象では、JavaScriptはボトルネックとならない場合が多いのです。多くのパフォーマンスの問題は描画の遅さとレンダリングタイムに由来します。DOMを使って画面をピクセルに変換して表示するため、ユーザがスクロールすると発生する多くの描画の作業が、結果として非常に遅い動きにつながるということです。

注:HTML5 Rocksではスクロールが遅い原因をいくつかピックアップして解説しています。もしこの問題に直面していると感じたなら、読んでみる価値はあります。

描画のパフォーマンスを計測する

フレームレート

私たちは、このページ上の何かがフレームレートに影響しているのではないかと疑いました。それで、Chromeのデベロッパツールを開いて、”Timeline”と”Frames”モードを選び、新たなセッションを記録してみることにします。”record”ボタンをクリックして、通常のユーザがするようにページをスクロールしてみます。今回は数分間使用することを想定して、もう少し早くスクロールしてみます。

Screen Shot 2013-05-15 at 17.57.48-500
スクロールインタラクションをプロファイルするためChromeのデベロッパツールを使う。拡大画像

上へ、下へスクロールします。上部のサマリービューに、描画とレンダリングタイムに呼応した紫色と緑色が多く見えますね。一度記録をストップします。これらの様々なフレームをパラパラと見てみると、相当重い”Recalculate Styles”と、たくさんの”Layout”が見つかりました。

レジェンドの右端を見れば、60FPSはおろか、ほとんど30FPSすら達成できていないということが分かるでしょう。パフォーマンスは最悪です。さて、このサマリービューのバーはそれぞれフレームに対応しています。つまり、Chromeにおけるすべての作業はアプリを画面上に描画することに関係しているのです。

screen434343-500
Chromeのデベロッパツールが描画にかかる長い時間を表示している。拡大画像

フレームの値

一般的にターゲットとするべき理想の数値は60FPSですが、これを目指すとすれば、共通して使用する端末のリフレッシュレートに合致させなくてはなりません。つまり16.7ミリ秒の中で全てを完了させなくてはならないということになります。JavaScript、レイアウト、イメージのデコード、そしてリサイズや描画、合成など、全てです。

注:理想とすべきは一定のフレームレートです。もし何らかの理由で60FPSを達成できないとしたら、その場合は30FPSから60FPSの間でフレームを可変にしておくよりは、30FPSを目標とした方がいいでしょう。ただし実際には、JavaScriptが全てのレイアウトを実行し終わった時描画とコンポジットにはまだ作業が残っていて、それを事前に予測するのは非常に難しいため、そのようにコードを書くのはなかなか難しいかもしれません。どちらにせよ、フレームレートがいくつであっても、常に一定であるように、変動しないようにしましょう(そうでないとガタガタに見えてしまいます)。

携帯電話などローエンドの端末をターゲットにしているなら、16ミリ秒のフレーム値は実際には8~10ミリ秒くらいでしょう。デスクトップマシンでも、様々なブラウザの処理がなされた結果、フレームの値が低くなり、同様になる可能性があります。もしこの値を下回ってしまったら、フレームが落ちてページ上にjankが出てきてしまうでしょう。8~10ミリ秒と言いましたが、実際の予測値を得るために、ターゲットとする端末でテストするようにしてください。

screen434343-500
500ミリ秒以上の非常に効率の悪いレイアウト処理。拡大画像

注:Chromeデベロッパツールを使ってレンダリングのパフォーマンス問題を発見し修正するという記事もあります。これは、よりタイムラインにフォーカスしています。

さてスクロールの話に戻りますが、私たちはonscrollで不必要な修復が多数起こっているのではないかということを疑いました。

ありがちなミスとしては、ページの onscrollハンドラに大量のJavaScriptを詰め込んでしまうということです。これではフレーム値をまったく達成できません。レンダリングパイプラインに作業をまとめる (例えばrequestAnimationFrame内に配置する)ことによりもう少し余裕ができますが、全てを行うのに数ミリ秒しかないのは変わりません。

最良の手段は、例えばスクロールハンドラのscrollTop の値をただキャプチャして、requestAnimationFrame コールバックの中の最新の値を使うことです。

矩形を描画する

ではDeveloper Tools → Settingsに戻って、”Show paint rectangles”を有効にしてください。これにより、描画されている画面のエリアがステキな赤色のハイライトで可視化されます。さてPinterestをスクロールするとどうなるか見てみましょう。

Screen Shot 2013-03-25 at 14.35.17-500
Chromeデベロッパツールの”Paint Rectangles”機能を有効にする。拡大画像
* アニメーションGIF

ミリ秒ごとに、大きく明るい赤色が画面全体のあらゆる箇所で点滅しています。スクロールするたびに毎回、全画面の描画が行われているようで、これはとても非効率的である可能性があります。望ましいのはブラウザがページ上の新しい項目だけ、すなわち通常はスクロールされるに従って下部と上部だけを描画するということになります。今回の原因は右下のコーナーにある”scroll to top”という小さなボタンのようです。ユーザがスクロールをすると、上部の固定されたヘッダ部分が再描画される必要があるのですが、このボタンもそうなのです。これに対し、Chromeは描画されるべき2カ所のエリアの和集合を作成します。

Screen Shot 2013-05-15 at 19.00.12-500
描画されたばかりのエリアをChromeは赤の枠線で示している。拡大画像

このケースでは、左上から右上にかけて赤い枠線で囲まれていますが、あまり高さはありません。それと下側の右のコーナーにも枠線があります。これでは左上から右下まで、つまり実質、全画面が囲まれてしまっています。デベロッパツールのボタンの要素を調べて、それを(Hキーを使って)非表示にしたり削除して、もう一度スクロールしてみれば、ヘッダのエリアのみが再描画されることが分かるでしょう。この特定の問題を解決する方法は、スクロールボタンを専用のレイヤに動かすことでヘッダと統合されないようにすることです。基本的にはこれでボタンが分離されますから、残りのページの上にコンポジットできます。レイヤとコンポジットについては後でもう少し詳しく話しますね。

さて次に気づくことはマウスオーバーに関わることです。ピンの上にマウスオーバーする時、Pinterestは”Repin, comment and like”ボタンを含むアクションバーを描画します。とりあえずこれをアクションバーと呼んでおきましょう。1つのピンの上にマウスオーバーすると、バーだけでなくその下層にある要素も描画されます。描画は、見た目が変わると期待する要素のみに起こるべきです。

Screen Shot 2013-03-25 at 14.35.46-500
問題の原因:全画面での赤のフラッシュは多くの描画が起こっていることを示している。拡大画像

ここでもう1つ、スクロールに関しておもしろいことがあります。このピンの上にカーソルをマウスオーバーさせておいて、もう一度ページをスクロールしてみましょう。

新しいイメージの列をスクロールするたび、マウスオーバーするつもりでなかった他のピン上でもこのアクションバーが描画されます。これは何よりもユーザ体験に影響することが多いのですが、このケースでは、スクロールのパフォーマンスのほうが、スクロール中のマウスオーバーのエフェクトよりも重要だと思われます。マウスオーバーすることにより、スクロール中にjankが増えてしまいます。基本的にブラウザはエフェクトを描画するために休止するからです(同じことが、私たちが要素からロールアウトする時にも言えます)。ここでの手は、少し遅らせてsetTimeoutを使うことで、そうすればユーザが使おうと意図する時にのみバーが描画されます。これは、”不必要な描画を避ける “の中でも説明されている手法です。もっと積極的な手法としては、マウスオーバーを有効化する前に、mouseenterやマウスの軌道を計測することです。ここまで計測するのはやりすぎのように思うかもしれませんが、ユーザがスクロールしている時は特に、不必要な描画は何をもってしても避けなくてはなりません。

全体の描画コスト

これで、ページ当たりの描画処理コストを見るための素晴らしいワークフローができました。それでは、デベロッパツールの“Enable continuous page repainting” に戻ってください。このオプションを有効にすると、スクリーン上で描画を何回も続け(repaint)、コストの高い描画要素を調査します。結果は右上に背景の黒いボックスの中に、描画の最大処理時間と最低処理時間が集計されて表示されます。

screenshot43234242-500
Chromeの”Continuous Page Repainting”モードはページの全描画コストの査定を手伝ってくれる。拡大画像

では、デベロッパツールの”Elements”パネルに戻りましょう。ここでノードが選択でき、キーボードを使ってDOMツリーが操作できます。コストの高いと疑われる描画要素は、Hキーを押せば一時的に無効にできます(Chromeに最近追加された機能)。Continuous Paintingモードを使用して、要素を無効にすることで、ページにプラスの効果があるのか瞬時に分かります。要素を非表示にすることで、処理時間の短縮が期待できるため、ページへのプラス効果が予想されます。しかし、これを実行することで特にコストの高い描画要素が特定できるため、さらなる精査が必要となります。

Screen Shot 2013-06-10 at 09.46.31_500_mini
描画処理に掛った時間が表示された”Continuous Page Repainting”ビューのスクリーンショット。

PinterestのWebサイトでは、カテゴリーバーやヘッダで描画要素の表示/非表示をトグルすることができます。必要のない描画要素を無効にすることで、スクリーン上に描画する時間が短縮されたことが分かります。もし、もっと詳しく知りたい場合は、timelineに戻り、新しいセッションを記録して効果を計測することができます。すごいですよね。ほとんどのページでこのワークフローが非常に役に立ちますが、そうでない場合もあります。Pinterestの場合、ピンのコードのネストが深いため、このワークフローで描画時間を計測するのは難しくなります。

幸いにも、要素(この場合はピン)を選択し、”Styles”のパネルで使用されているCSSスタイルを確認することで、より多くの事ができます。要素の表示/非表示をトグルすることで、描画時間にもたらす効果を見ることができます。このことからページの描画プロファイリングのより詳細な見識を得ることができます。

ここで、Pinterestがピンにbox-shadowを使用しているのが分かります。過去2年に渡りChromeのbox-shadowのパフォーマンスの最適化をしてきましたが、他のスタイルとの組み合わせや使用頻度の高さによってはボトルネックの原因となるため、見る価値はあります。

Pinterestはbox-shadowborder-radiusを含まない別の要素に移すことで、Continuous Paintingモードの時間を40パーセント短縮しました。これには角が少しぼやけてしまう欠点はありますが、色彩設計とborder-radiusの値が低いため顕著ではありません。

注:詳しくは”CSS Paint Times and Page Render Weight” をお読みください。

Screen Shot 2013-03-25 at 15.47.40-500
レンダリングの重さへの影響を計測するため、スタイルをトグルしている。拡大画像

box-shadowを無効にして変化はあるのか見てみましょう。見てのとおり、ピンに影が見えなくなりました。では、timelineに戻り、前のように(上下を繰り返す)スクロールを新しいセッションとして記録します。たった1つの変更で毎秒60 フレームに近づきました。

公共広告:box-shadowを使わないでくださいとは言いません。大いに使用してください。ただ、パフォーマンスに問題がある場合は、どこにボトルネックがあるのか特定できるよう正しく計測してください。必ず計測してください。Webサイトやアプリケーションは作成者特有であるため、ボトルネックもそれに応じて多様になります。ほぼ毎日ブラウザ内部は変化するので、計測して最新の情報を知るのが一番賢い方法です。さらに、Chromeのデベロッパツールがこれを簡単にします。

Screen Shot 2013-03-25 at 15.50.25-500
Chromeデベロッパツールを使用してブラウザのパフォーマンスの変化を分析する方法が最も効果的。拡大画像

注: Eberhard Grather が”Profiling Long Paint Times With DevTools’ Continuous Painting Mode” で詳しくプロファイリングについて投稿しています。プロファイリングにきちんと時間を掛けるべきだと思います。

さらに、”repin(リピン)”ボタンをクリックすると、アニメーション効果とLightBoxが描画処理されていることに気が付きました。バックグラウンドに再描画の大きな赤い点滅があります。この再描画が、白いカバーなのか、その他の影響を受けた場所なのか、ツールからは明らかではありません。見た目だけではなく、描画発生範囲を示す矩形が再描画要素(あるいは再描画される思われる要素)と一致するか必ず再確認してください。この場合、スクリーン全体が描画処理されたように見えますが、単に白で覆われていることも十分あり得るため、コストは高くないかもれません。微妙な差異のため、見ているものが何なのか、なぜなそうのかを良く理解することが重要です。

ハードウェアコンポジティング(GPUアクセラレーション)

Pinterestのケーススタディの最後としてGPUアクセラレーションを見てみましょう。以前ブラウザは主にCPUでWebページのレンダリングをしていました。その際、まず、レイヤと呼ばれるグループ化した要素テクスチャを描画し、その後で全てのレイヤを合成してスクリーンで表示する画像にしていました。

GPUを合成プロセスに含むことで処理の大幅な高速化が可能性になることが、近年分かってきました。そこで前提になるのが、CPUがテクスチャを描画処理している間に、合成できるようGPUに渡すことができることです。これからのフレームで要素を(CSS transitionあるいはCSS animationを使って)動かすか、不透明度を変えるだけなのであれば、これらの変更をGPUに指示するだけで全てのことを行ってくれます。基本的にはGPUに新しいグラフィックを渡すのではなく、ただ既存のグラフィックを動かすよう指示します。これをGPUは非常に速く処理できるため、全体のパフォーマンスを改善することになります。

使用しているプラットフォームでハードウェアコンポジティングが利用可能あるいは有効とは限りません。しかし、例えば、初めて要素の3D変換の指示をしたりする時に利用可能だとすれば、Chromeでは有効になります。多くの開発者は translateZハックを使ってこれを行います。しかし、希望してもしなくても、変換した要素に独自のレイヤが生成されてしまう欠点があります。ただ、他の要素が影響されて再描画されないように、効果的にその要素を隔離できる利点もあります。覚えておくと良いのは、これらテクスチャのシステムメモリからビデオメモリへの転送は必ずしも高速ではないと言うことです。レイヤが多ければ多いほど、多くのテクスチャの転送が必要になり、多くのレイヤの管理も必要になります。そのため、このハックの使い過ぎには気を付けてください

注:Tom WiltziusがChromeのレイヤモデルについて書いているので、どのようにバックグラウンドでコンポジティング処理が行われているのか興味がある場合は読んでみてください。Paulも translateZハックの投稿 で、正しい使い方について説明しています。

さらに、デベロッパツールの設定で”Show composited layer borders”が役に立つと思います。この機能はGPUで操作されているDOM要素の見識を深めるのに良いと思います。

layer_folders_addy_500_mini
Show composited layer bordersを有効するとChromeのレンダリングレイヤを表示。拡大画像

この機能が有効であれば、GPUアクセラレーションの影響を受けている要素はオレンジの枠線で囲まれます。このページでスクロールしても、”Scroll to top”ボタンを押したりや他の操作をしても、コンポジットレイヤが使用されている形跡は見られません。

Chromeはバックグラウンドで自動的にレイヤ化する処理機能が良くなってきています。しかし、前にも言いましたが、開発者はtranslateZハックを使用してコンポジットレイヤを作成します。下記はtranslateZ(0)を全てのピンに適用したPintrestフィードです。毎秒60フレームには達していませんが、近づいてはおり、一定して30FPSをデスクトップで維持できるようになっています。悪くないですよね。

Screen Shot 2013-05-15 at 19.03.13-500
translateZ(0)ハックを全てのPinterest のピンに適用。オレンジの枠線に注目。拡大画像

パフォーマンスの特徴は大幅に違ってくるとは思いますが、デスクトップとモバイルの両方で必ず試してください。両方でtimelineを使って、Continuous Paintモードの描画時間の表を見て、どれだけの計算時間を、どれだけの早さで使ってしまっているか見積もってください。

改めて言いますが、このハックを全てのページの要素には使わないでください。デスクトップの基準にはパスしますが、モバイルではパスしません。理由は、ビデオメモリの使用の増加とレイヤ管理コストの増加が起こるためです。これらはどちらもパフォーマンスに悪い影響を及ぼす可能性があります。代わりに、描画コストが比較的高い要素を隔離するためだけにハードウェアコンポコンポジティングを使ってください。

WebKit nightliesでは、レイヤをコンポジットする理由をもWeb Inspector(デバッグツール)で提示してくれるようになりました。この機能を有効にするには、”Use WebKit Web Inspector”オプションをオフにすることでフロントエンドに表示されます。”Layers”ボタンを使ってオンにしてください。

問題発見から修正までのワークフロー

Pinterestのケーススタディはここまでですが、自分が直面する描画の問題にはどのような手順で分析および対処すれば良いのでしょうか。

問題を見つける

  • ブラウザでは必ず”シークレット”モードを使ってください。拡張機能やアプリによってパフォーマンスのプロファイリングの数値が正しくなくなることがあります。
  • ページとデベロッパツールを開いてください。
  • timelineでページのセッションを記録し、ページを操作してみてください。
  • 描画時間がオーバーする(=60FPSの描画に間に合わない)フレームがないか確認してください。
  • 描画時間がギリギリの場合は、モバイルの方ではかなりの時間オーバーになっているはずです。
  • jankの原因を確認してください。描画処理やCSSレイアウト、JavaScript の実行に時間が掛っていないか確認してください。

Screen Shot 2013-05-15 at 19.36.22-500
ChromeデベロッパツールのFrameモードで自分のWebサイトの実行時間の調査に十分時間を掛けること。拡大画像

問題を修正する

  • “Settings”で”Continuous Page Repainting”を有効にしてください。
  • “Elements”パネルで(H)ショートカットを使って必要ないものは非表示にしてください。
  • DOMツリーを操作して、要素を非表示にしたりtimelineでFPSを確認したりしてください。
  • どの要素が描画時間を長くしているのか割り出してください。
  • 描画時間に影響ありそうなスタイルはオフにして、FPSを監視してください。
  • 遅延の原因となる要素を特定するまでこれらを続けてください。

fixing-500_mini
他のデベロッパツール機能も使用して調べること。拡大画像

他のブラウザ

これを書いている今現在、Chromeの描画パフォーマンスのプロファイリングツールが一番なのですが、他のブラウザでもテストと計測をして、ユーザ体験(実行できているか)を確認することを強く推奨します。パフォーマンスはブラウザによってかなり変わってくるので、あるブラウザで起きた事象でも別のブラウザでは起きない可能性があります。

前にも書きましたが、推測するな、テストしろ!です。自分で計測し、抽象を理解してブラウザの内部を知っておいてください。そのうちに、この分野で使用するツールが改善され、ブラウザの種類に関係なく開発者が正確にレンダリングパフォーマンスを確認できるようになること期待します。

まとめ

パフォーマンスは重要です。マシンはそれぞれ異なり、開発者が使用する処理の速いマシンでは発生しない問題でも、ユーザの使用している端末では発生するかもしれません。フレームレートはエンゲージメントに大きく影響し、最終的にはプロジェクトの成功にも影響します。幸いにも、フレームレートのための素晴らしいツールがたくさんあります。

描画パフォーマンスの計測は必ずデスクトップとモバイルの両方で行ってください。うまく行けば、使用している端末に関係なく、ユーザにとって滞りなく滑らかな操作ができるページを作成できます。

著者について

Addy OsmaniPaul Lewisは、それぞれツールとレンダリングパフォーマンスを担当するChromeのデベロッパリレーションズのエンジニアです。それらに問題が生じている時以外は、他の開発者が滞りなく滑らかなWeb体験を実現できるよう助けています。