GPUアニメーション:正しく実行する

誰もが、最新のブラウザーがページの一部をGPUで描画できることを既に知っていると思います。 これは特にアニメーションで顕著です。 たとえば、CSS transform



プロパティを使用して作成されたアニメーションは、 top/left



を使用して作成されたアニメーションよりもはるかに美しく滑らかに見えます。 ただし、「GPUでアニメーションを正しく作成する方法」という質問には、通常、「use transform: translateZ(0)



またはwill-change: transform



」などのように答えられます。 これらのプロパティは既にzoom: 1



ようなものになっていますzoom: 1



IE6の場合は(ブラウザーの開発者が呼び出すことを好むので、GPUまたはコンポジション (合成)でアニメーション用のレイヤーを準備するためにIE6の場合)







ただし、単純なデモで美しくスムーズに機能するアニメーションは、完成したサイトで突然スローダウンし始め、さまざまな視覚的なアーティファクトを引き起こしたり、さらに悪いことにブラウザがクラッシュしたりします。 なぜこれが起こっているのですか? これに対処する方法は? この記事を理解してみましょう。







1つの大きな免責事項



GPUコンポジションの詳細を検討する前に、私が言いたい最も重要なことは、 これはすべて1つの巨大なハックです。 W3C仕様(少なくとも現時点では)には、GPUコンポジションプロセスの説明、要素を別のレイヤーに明示的に転送する方法、またはコンポジションモード自体さえありません。 これは、いくつかの一般的なタスクをスピードアップするための単なる方法であり、すべてのブラウザー開発者が独自の方法で実行します。 この記事で読むすべては公式の説明ではありませんが、いくつかのブラウザーサブシステムの操作に関する常識と知識に長けた実験と観察の結果です。 何かが真実ではないかもしれませんが、何かは時間とともに変化します-私はあなたに警告しました!







作曲の仕組み



GPUアニメーション用のページを適切に準備するには、インターネットまたはこの記事にあるヒントに従うだけでなく、ブラウザー内での動作を理解することが非常に重要です。







position: absolute



と異なるz-index



である要素A



B



を持つページがあるとしz-index



。 ブラウザはページ全体をCPUに描画し、結果の画像をGPUに送信し、そこから画面に到達します。







 <style> #a, #b { position: absolute; } #a { left: 30px; top: 30px; z-index: 2; } #b { z-index: 1; } </style> <div id="#a">A</div> <div id="#b">B</div>
      
      





例1







CSS Animationsを使用して、CSS left



プロパティを介して要素A



の動きをアニメーション化することにしました。







 <style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { left: 30px; } to { left: 100px; } } </style> <div id="#a">A</div> <div id="#b">B</div>
      
      





例1-2







この場合、アニメーションの各フレームに対して、CPU側のブラウザーは要素のジオメトリを再カウント(リフロー)し、ページの現在の状態で新しいイメージを描画(リペイント)し、GPUに送信した後、画面に表示されます。 再描画はかなり高価な操作であることがわかっていますが、最新のブラウザはすべて、画像全体ではなく、変更された部分のみを再描画するのに十分スマートです。 そして、彼らはそれを十分に速くしますが、アニメーションはまだ滑らかさを欠いています。







アニメーションの各ステップのページ全体の部分的ではあるが、ジオメトリの再計算と再描画:特に大規模で複雑なサイトでは、非常に時間がかかる操作のように見えます。 要素A



と要素A



ないページ自体の2つの画像を一度描画してから、これらの画像を相対的に移動するだけの方がはるかに効率的です。
つまり、キャッシュされた要素の画像の構成を作成する必要があります。 そして、これはまさにGPUが最もよく処理するタスクです。 さらに、彼はサブピクセル精度でそれを行う方法を知っており、非常に滑らかなアニメーションを提供します。







コンポジションで最適化を適用するには、ブラウザはアニメーション化されたCSSプロパティを確認する必要があります。







  1. 文書の流れに影響を与えることはありません。
  2. 文書の流れに決して依存しない。
  3. 要素自体を再描画する必要はありません。


横から見ると、 top



position: absolute/fixed



left



プロパティはposition: absolute/fixed



と一緒に外的要因に依存していないように見えるかもしれませんが、そうではありません。 たとえば、 left



プロパティは、 .offsetParent



のサイズに依存するパーセント単位の値と、環境に依存するem



vh



などの単位を取ることができます。 したがって、説明に適合するのは、 transform



opacity



CSSプロパティです。







アニメーションを作り直しましょう: left



代わりに、 transform



をアニメーション化しtransform









 <style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { transform: translateX(0); } to { transform: translateX(70px); } } </style> <div id="#a">A</div> <div id="#b">B</div>
      
      





コードに注意してください。 アニメーション全体を宣言的に説明しました:開始、終了、継続時間など。 これにより、ブラウザは、アニメーションが開始される前でも、要素のどのCSSプロパティが変更されるかを正確に決定できます。 これらのプロパティの中にリフロー/リペイントに影響するものがないことを確認すると、ブラウザはコンポジションで最適化を適用できます。2つの画像を描画してGPUに転送します。







例2







そのような最適化の利点:









すべてがシンプルで明確なように思えますが、どのような問題が発生する可能性がありますか? そのような最適化が実際に行うことを見てみましょう。










これは一部の人にとっては啓示かもしれませんが、GPUは別のコンピューターです。 はい、すべての最新デバイスの不可欠な部分は、実際には、独自のプロセッサ、メモリ、および情報処理方法を備えた独立したサブシステムです。 また、ブラウザは、他のプログラムやゲームと同様に、別のデバイスと同様にGPUと通信する必要があります。







これをよりよく理解するには、AJAXを覚えておいてください。 たとえば、ユーザーが承認フォームに入力したデータに従ってユーザーを登録する必要があります。 サーバーはブラウザのメモリにアクセスできないため、リモートサーバーに「これらのフィールドとその変数からデータを取得してデータベースに保存する」と伝えることはできません。 代わりに、ページから必要なデータを単純なデータ形式(JSONなど)である種のペイロードに収集し、サーバーに送信します。







同じことが作曲中にも起こります。 GPUは実際にはリモートサーバーであるため、CPU側のブラウザーは最初に特別なペイロードを準備してから、デバイスに送信する必要があります。 はい、GPUはCPUに非常に近いですが、AJAXを介して応答を送受信するのに2秒かかることが多い場合、GPUにデータを転送するための余分な3-5ミリ秒がアニメーションの品質に深刻な影響を与える可能性があります。







GPUのペイロードは何ですか? 原則として、これらはレイヤーの画像と、レイヤーのサイズ、相互の相対的な位置、アニメーションの指示などを決定する追加の指示です。 負荷を作成してGPUに転送するプロセスは次のようになります









したがって、magic transform: translateZ(0)



またはwill-change: transform



要素に追加するたびwill-change: transform



プロセス全体が開始されます。 再描画はリソースを大量に消費するタスクであることは既に知っています。 ただし、この場合はさらに悪いことです。多くの場合、ブラウザはインクリメンタル再ペイントを適用できず、変更された部分のみを再描画できません。 彼は、新しいレイヤーによって隠されたパーツを再描画する必要があります。







例3







暗黙の構成



要素A



B



例に戻りましょうB



以前は、ページ上の他のすべての要素の上にある要素A



をアニメーション化しました。 その結果、2つのレイヤーのコンポジションが得られましたA



のレイヤーとB



レイヤーとページの背景です。







タスクを変更しましょう:要素B



をアニメーション化します...







例4







...そして論理的な問題があります。 要素B



は別の複合レイヤー上にある必要があり、ユーザーに表示される画像の最終的な構成はGPU上にあります。 ただし、まったく触れない要素A



、視覚的要素B



上にある必要がありますB









One Big Disclaimerを思い出してください。CSSにはGPUコンポジション用の特別なモードはなく、特定の問題を解決するための最適化にすぎません。 z-index



指定された順序で要素A



B



正確に取得する必要がありz-index



。 この場合、ブラウザは何をすべきですか?







そうです:要素A



を別の複合レイヤーに転送します! したがって、別の重い再ペイントを追加します。







例4-2







これは暗黙的コンポジションと呼ばれます。コンポジットエレメントより上のz-index



1つ以上の非コンポジットエレメントもコンポジットになります。つまり、個別のイメージに描画され、GPUに送信されます。

暗黙のコンポジションは、思っているよりもはるかに頻繁に発生します。ブラウザは、多くの理由で複合レイヤーに要素をもたらします。









詳細については、ChromiumプロジェクトのCompositingReasons.hファイルを参照してください。







一見すると、GPUアニメーションの主な問題は、予想外の大量の再描画であるように思えるかもしれません。 しかし、これはそうではありません。 最大の問題は...







メモリ消費



また、GPUは別のコンピューターであることを思い出してください。 レンダリングされたレイヤー画像は、GPUに転送するだけでなく、GPUに保存する必要があるため、美しくアニメーション化できます。







1つのレイヤーの画像の重量はどれくらいですか? 例を見てみましょう。 単色#ff0000



で塗りつぶされたサイズ320×240の通常の長方形のサイズを推測してみてください。







直します







通常、Web開発者は次のように考えます。「これは単色の画像です... PNGで保存し、サイズを確認します。1キロバイト未満である必要があります。」 そして、彼らは正しいでしょう:この画像は本当にPNGでわずか104バイトの重さです。







しかし問題は、PNG(JPEG、GIFなど)がデータを保存および送信するための形式であることです。 このような画像を画面に描画するには、コンピューターがそれを解凍し、 ピクセルの配列として提示する必要があります。 したがって、コンピューターのメモリ内のイメージは、 320×240×3 = 230,400バイトを占有します 。 つまり、画像の幅に高さを掛けて、ピクセル数を取得します。 次に、各ピクセルの色が3バイト(RGB)で記述されるため、ピクセル数に3を掛けます。 画像が半透明の場合、透明度値(RGBA)を記述するために320×240×4 = 307,200バイトの別のバイトが必要なので、4を掛けます







ブラウザーは常に RGBAイメージで複合レイヤーをレンダリングします。明らかに、レンダリングされたDOM要素に透明な領域があるかどうかを自動的に判断するのに十分で効果的な方法はありません。







典型的な例を考えてみましょう:サイズが800×600の写真10枚のカルーセル。 カルーセル内の画像をスムーズに変更することにしたので、 will-change: transform



写真ごとに事前にwill-change: transform



を指定し、JSを使用して、ドラッグアンドドロップなどのユーザーアクションへの遷移をアニメーション化しました。 このようなページを単純に表示するために必要な追加メモリの量を計算してみましょう:800×600×4×10≈19 MB







1ページあたり1つのコントロールを描画するには、19 MBの追加メモリが必要でした。 また、多くのアニメーションコントロール、視差効果、網膜画像、その他の視覚的要素を備えたSPAページに対する現代の開発者の愛情を考えれば、ページあたり100〜200 MBの追加は限界をはるかに超えています。 ここに暗黙の構図を追加します(これを前に考えたこともありませんか?:)。非常に悲しい画像が表示されます。







さらに、まったく同じ結果を表示するためだけに、この追加メモリが無駄になることがよくあります。







例5







また、デスクトップクライアントの場合、これはまだそれほど顕著ではありませんが、モバイルデバイスの場合、この問題は特に深刻です。 第一に、ほとんどすべての最新デバイスで高ピクセル密度の画面が使用されています。レイヤー画像に別の4〜9を掛けます。 第二に、そのようなデバイスはデスクトップと比較してかなりのメモリを持っています 。 たとえば、まだそれほど古くないiPhone 6のメモリは1 GBのみであり、RAMおよびVRAM(GPU用のメモリ)に共通です。 せいぜいこのメモリの3分の1がシステムプロセスとバックグラウンドプロセスで使用され、さらに3分の1-ブラウザと現在のページで使用されることを考えると(そして、これはダースフレームワークを使用せず、すべてを非常に最適化するという条件で)、GPU特殊効果のために残ります約200〜300 MB。 さらに、iPhone 6は、より手頃な価格のメモリデバイス上にある高価なハイエンドデバイスを指します。







合理的な質問があるかもしれません: PNG画像をGPUに保存してメモリを節約することは可能ですか? はい、技術的には可能ですが、GPUの特徴は、 各レイヤーがピクセルごとに描画されることです。 つまり、画面に1ピクセルを描画するには、PNG画像を毎回再デコードして必要な色を取得する必要があります。 この場合、最も単純なアニメーションの速度が1 fpsを超えることはほとんどありません。







GPUには独自の画像圧縮形式がありますが 、圧縮率に関してはPNGやJPEGにも近くないことに注意してください。GPU自体のサポートにより、それらの使用の可能性は制限されています。







長所と短所



GPUでのアニメーションの動作の理論的な部分を調べたので、便宜上、それらの使用のすべての長所と短所を集めましょう。







のために





に対して





ご覧のとおり、GPUアニメーションには、その独自の利点すべてに対して、多くの非常に重大な欠点があります。その主なものは、再描画とメモリ消費です。 したがって、すべての最適化はこれらの2つのポイントに正確に関連付けられます。







環境をカスタマイズする



高品質のアニメーション用にサイトを最適化する前に、最適化の結果だけでなく問題のある領域も表示する特別なツールを用意する必要があります。







サファリ



Safari Web Inspectorには、ページ上のすべての複合レイヤー、それらが消費するメモリを確認できる優れたツールが組み込まれています。また、要素を別のレイヤーに移動する理由を表示することもできます。 このツールを表示するには:







  1. Safariで、⌘⌥Iを使用してWeb Inspectorを開きます。 動作しない場合は、[設定]> [詳細]で[メニューバーに [ 開発メニューを表示]オプションを有効にして再試行してください。
  2. Webインスペクターで、[要素]タブをクリックし、右側の列で[レイヤー]を選択します。
  3. これで、(要素)タブの主要部分の要素をクリックすると、右側に、この要素の複合レイヤーに関する情報(存在する場合)と、レイヤーを持つすべての子要素のリストが表示されます。
  4. 子レイヤーをクリックすると、構成の理由が表示されます。ブラウザーが要素を別の複合レイヤーに移動することを決定した理由です。


Webインスペクターを備えたSafari







Google Chrome



DevToolsにも同様のツールがありますが、それを有効にするには特別なフラグを設定する必要があります。







  1. ブラウザで、 chrome://flags/#enable-devtools-experiments



    に移動し、 Developer Toolsの実験フラグを有効にします
  2. ToolI(Mac)またはCtrl-Shift-I(PC)でDevToolsを開き、アイコンをクリックします DevToolsを使用しましたChrome 右上の[設定]セクションに移動します。
  3. メニューから[実験]セクションを選択し、[レイヤー]パネルをオンにします。
  4. DevToolsを再度閉じて開きます:レイヤーパネルが利用可能になります。


DevToolsを使用しましたChrome







このパネルには、アクティブなすべての複合ページレイヤーがツリービューで表示されます。 レイヤーを選択すると、そのレイヤーに関する情報が利用可能になります:サイズ、占有メモリ量、再描画の回数、および複合レイヤーへの削除の理由。







最適化



そのため、環境を設定し、最適化に直接進むことができます。 コンポジットレイヤーの使用に関する2つの主な問題を既に特定しました。追加の再ペイント(その後、レイヤーイメージをGPUに転送する必要がある)とメモリ消費です。 したがって、すべての最適化は、再描画サイクルとメモリ消費の削減を目的としています。







暗黙の構成を避ける



非常にシンプルで明白ですが、最も重要な最適化。 暗黙のコンポジションとは、GPUで別の明示的な複合レイヤー(ビデオ、CSSアニメーションなど)と正しく結合するためにのみ、個別の複合レイヤーに要素を削除することです。 この問題は、アニメーションを開始するときにモバイルデバイスで特に強く感じることができます。







小さな例を考えてみましょう。









ユーザーアクションによってアニメーション化する要素A



があります。 レイヤーツールを使用してページを見ると、追加のレイヤーがないことがわかります。 ただし、[再生]ボタンをクリックするとすぐに、いくつかの合成レイヤーが表示され、アニメーションの終わりまでに消えます。 タイムラインツールを見ると、アニメーションの開始と終了にページの重要な領域の再描画が伴うことがわかります。







クロムタイムライン







この例でブラウザが何をしたかをステップごとに見てみましょう。







  1. , , . — .
  2. Play A



    — CSS Transition transform



    . , z-index



    A



    B



    . .
  3. repaint. -, - , - — . .
  4. GPU, , . , , GPU . «» : .
  5. A



    . , , – A



    B



    . , ( repaint) GPU. , .4, «».


, :









transform



opacity





, , GPU. , , , , , , 3D-. .







: . , :







 <div id="bg-change"></div> <style> #bg-change { width: 100px; height: 100px; background: red; transition: background 0.4s; } #bg-change:hover { background: blue; } </style>
      
      





CPU, repaint . GPU: :







 <div id="bg-change"></div> <style> #bg-change { width: 100px; height: 100px; background: red; } #bg-change::before { background: blue; opacity: 0; transition: opacity 0.4s; } #bg-change:hover::before { opacity: 1; } </style>
      
      





, . - .









. ?







例6







, 40 000 (39 ), — 100 , 400 . なんで? :







 <div id="a"></div> <div id="b"></div> <style> #a, #b { will-change: transform; } #a { width: 100px; height: 100px; } #b { width: 10px; height: 10px; transform: scale(10); } </style>
      
      





, #a



— 100×100 (100×100×4 = 40 000 ), #b



— 10×10 (10×10×4 = 400 ), 100×100 transform: scale(10)



. #b



- will-change



, transform



GPU .







: width



height



, transform: scale(…)



. , . , , , 5–10% : , .







CSS Transitions Animations



, transform



opacity



CSS Transitions Animations GPU. JS, , , , : translateZ(0)



transform



, will-change: transform, opacity



, .







JS- , requestAnimationFrame



. Element.animate()



CSS-.

CSS Transitions/Animations , — JS , CSS, .







? , JS - ?







CSS- : GPU . , , GPU. JS , — . 60 ( JS ) GPU, . , , CSS-, :









, , JS . CSS- , ( ), JS .







CSS, . , JS.









, Chaos Fighters . - . , , GPU, , . iPhone 5 — Apple — . .







, , .







, - . : CSS-. — : , <img>



CSS- :









. — .







. , , . , . , .







: .sun



, ; .









, , . : 500×500×4 ≈ 977 .







, 500×500 , , ( ) , 3000×3000×4 = 36 ! …

Layers . : . , GPU. - (), — .







, , ! , .







, : GPU , . , :









2 . , , . , GPU, .







, CSS , . transform



. 360˚. , @keyframes



, .







JS-, , ..









, 2 .







. — , , . . , . c GPU , : .







10%. 250×0.9 × 40×0.9 = 225×36 . , 250×20 , 250/225 ≈ 1.111.







: background-size: cover;



.sun-ray



, , transform: scale(1.111)



.









, , PNG- . , , , PNG-.







GPU 225×36×4 × 12 ≈ 380 ( 469 ). 19% , downscale



. 0.1



, , , , 977 / 380 ≈ 2.5 !







, , : CPU, JS-. , , . , GPU, . .









, Chaos Fighters, . :









One Big Disclaimer : GPU- -. , . , Google Chrome CPU GPU, , . Safari (, background-color



) CPU GPU , .







, GPU .







English version








All Articles