世界を動かす技術を、日本語で。

高さをアニメーションしない

概要

Granolaアプリで 高いCPU・GPU使用率 の原因が 小さなCSSアニメーション であることを発見。 Chrome DevTools で高コストなアニメーションの特定方法と最適化手法を解説。 height などのレイアウトプロパティのアニメーションが特に高コストである理由を説明。 transform を使ったアニメーション最適化による大幅なパフォーマンス改善例を紹介。 ブラウザの レンダリングパイプライン 理解がパフォーマンス向上に不可欠であることを強調。

Granolaアプリの高負荷問題と原因特定

  • Granolaは Electron製ノートアプリ
  • M2 MacBook でCPU 60%、GPU 25%使用を確認
  • Chrome DevTools のPerformanceタブで、主な処理はJavaScriptでなく「Rendering」と「Painting」
  • Layersタブで「アクションバー」レイヤーが毎フレーム再描画されている
  • Renderingタブで「Paint Flashing」「Layout Shift Regions」を有効化し、 音量ビジュアライザー のアニメーションが原因と判明
  • ビジュアライザーの「踊るバー」3本が常に再レイアウト・再描画される現象
  • DOM更新頻度を下げても効果なし。 CSSアニメーション がボトルネック
  • Performanceタブで「Animation」ジョブ3つ(バーごとに1つ)を確認
  • 問題のCSS:transition: height 300ms ease-in-out;

ブラウザのレンダリングパイプラインとアニメーションコスト

  • レンダリングパイプラインは Layout → Paint → Composite の順
  • height アニメーションは「Layout」再計算→「Paint」→「Composite」と全工程が発生
  • レイアウトプロパティ(例:height, width, top, left)はアニメーションコストが非常に高い
  • ペイントプロパティ(例:SVGのfillやstroke)はレイアウトなしでPaintとComposite
  • 最も安価なプロパティは Compositeプロパティ (例:transform, opacity)
    • これらはLayoutもPaintも発生せずCompositeのみ

transformによるアニメーション最適化

  • heightアニメーションtransform: scaleY() に置換することでパフォーマンス大幅改善
  • ただし、 scaleY は角丸が歪むなど見た目の問題が発生
  • 解決策として、 2つの矩形を上下から移動 させて1本のバーに見せる手法を採用
    • 各端に1つずつ矩形を配置し、それぞれ逆方向にtranslate
  • これにより、 Layout・Paintフェーズをスキップ し、Compositeのみで動作
  • 最適化後、CPU・GPU使用率が大幅減少(CPU 6%、GPU 1%未満)

パフォーマンス改善のポイントと今後

  • レンダリングパイプライン理解適切なCSSプロパティ選択 が重要
  • Chromeの about://tracing ツールでさらに詳細なパフォーマンス分析が可能
  • Granolaの最適化版ビジュアライザーは実際にアプリで体験可能
  • パフォーマンス最適化に興味がある方は採用情報も要チェック

Hackerたちの意見

簡単なアニメーションGIFのパフォーマンスが、合成レイヤーの変換の代わりにどうなるのか気になるなぁ。

GIFは歴史的に見ても、表示の観点からあまり最適化されてなかったから(中程度にアクティブなTumblrダッシュボードを持ってる人なら誰でもわかるよね)、もし悪化してたとしても驚かないよ。

もし実際にコントロールしたいなら、SVGやそれに基づくアニメーションがいいかも。

バーが実際のデータに関連してなくて、ただのプリセットアニメーションなら、GIFは一度のキャッシュダウンロードのために追加のファイルサイズが必要になるだけだよ。もしバーがリアルタイムのデータに合わせてアニメーションする必要があるなら、GIFはそのニーズには合わないと思う。

個人的には、リソースを消費するアニメーションやぼやけたGIF/canvasにはイライラするな。InfisicalはUIのアイコンに後者(canvas)を使ってるけど、あんまり好きじゃない。シャープで静的なアイコンの方がいいな。

ちょっとバカな質問だけど、親のdivを絶対位置にしたら合成の問題が解決するかな?

もうそうだったよ。これの全体のポイントは、高さの変更がレンダリングパイプラインの高コストなフェーズの一部だってことなんだ。

驚くのは、M2 Macのリソースがウェブサイトのレンダリングにこんなに使われることだよね。全てをゼロからレンダリングしても、数十年前のビデオゲームに比べたらグラフィックコンテンツはほとんどないのに、あっちの方が古いハードウェアでフレームを半分の時間で簡単にレンダリングしてたのに。

まあ、生のグラフィックコンテンツは簡単な部分だよね。ライブレイアウトツリーは再描画や合成が必要で、アクセシビリティツリーみたいな任意のレイヤーと交差しなきゃいけないから、N:Nスタイルの計算とは全然違うアーキテクチャなんだ。DOMをキャンバスで置き換えるっていうアイデアを試してる人たちの例を考えてみて。基本的なテキスト選択すらうまくいかなくなるからね。

アクティビティモニターのパーセンテージは、コアごとに100%だから、10コアのCPUだと最大で1000%の使用率になるよ。だから60%っていうのは、全体のCPUじゃなくて、1つのコアの60%ってことね。

この場合、以下のリンクも見てみるといいよ:https://developer.mozilla.org/en-US/docs/Web/CSS/contain https://developer.mozilla.org/en-US/docs/Web/CSS/will-change アニメーションのレイアウトプロパティに影響を与えるヒントをブラウザに提供できるかもしれないから。

これについてコメントしようとしてたんだけど、先に言われちゃった!親要素にcontain: strict;を設定すれば十分だったと思うよ。

CSSのcontainは、たくさんのパフォーマンスの「真実」を古くさくしてしまったけど、開発者の多くはそれに気づいていないんだよね。テキストエディタをWebGLに移植するための大規模な努力を見たけど、テキストのラスタライズが大変なところで、DOMバージョンの個々の要素に contain: content を付けるだけで、ほとんどのパフォーマンス向上が得られたはずなのに。ブラウザのレンダリングエンジンは、今や高度なGPUアクセラレーションされたコンポジタになってる。contain: strictを使った絶対位置指定は、パフォーマンスの方程式からほぼすべてのCSSレイアウトを取り除いてくれるし、自分でコンポジタやラインレイアウトを書く必要もない!* {contain: content} とフレックス/Gridを使えば、HTMLの良い部分と非常に良いパフォーマンスが得られるよ。

これにはちょっと頭がクラクラしてる。もし今の高さアニメーションがそんなに高コストなら、20年前はどれだけ高コストだったか想像してみて。運良く、似たようなトランスフォーム技術に切り替えたから、かなり前に高さのアニメーションをやめたんだけど…うーん、こんな一般的な操作の規模を知るのは本当に驚きだね。

本当にシンプルだよ。レイアウトをアニメーションさせないこと、特にDOMが巨大な場合はね。要素がレイアウトフローの外に絶対位置で浮いているなら、高さをアニメーションさせるのは問題じゃないよ。そして、特に自動からの高さや最大高さをアニメーションさせるためのブラウザの機能が良くなれば、もっと一般的になるはず。今のところ、一般的とは言えないけど。一般的な要求ではあるけど、レイアウトを考え始めるとそうじゃなくなる。ちなみに、マージンやパディングをアニメーションさせることは可能で、高さをアニメーションさせるのと同じくらいのジャンクを引き起こす可能性があるよ。

問題の本質は、高さをアニメーションさせるのが高コストってことじゃなくて、そのアニメーションがページのレイアウトを繰り返し変えたり、再描画を強制する影響なんだよね。

でも、20年前は高くなかったよ。20年前は、人々は限られた計算資源を効率的に使う方法を実際に知っていたんだ。

ユーザーとしてこれを制限することってできるのかな?ウェブページが一定のレンダリング/ペイント時間やリソースを超えないように強制するとか、そうしないと一つのバカなタブが全部のバッテリーを使い果たすとか。そうすれば、実際に気に入っているウェブページのためにリソース使用を許可できるし。「このウェブページは多くのリソースを使用しています」ってポップアップは見たことあるけど、今回は起こらないと思う。正直、これは恐ろしいと思う。作者が「固定」と決めた6%すら使うより、灰色から赤の「録画中」ドットに切り替わる方がいいな。99%のケースではUIデザイナーの「芸術的ビジョン」なんて全く気にしないし、残りの1%(例えばブラウザ内ゲームや便利なデータビジュアライゼーション)では、タブがリソースを使いまくるのを許可することができる。

あなたのブラウザは、ウェブ上のすべてのCSSをオーバーライドできるはずだよ。FirefoxでCSSアニメーションを無効にする方法はこれだよ:https://news.ycombinator.com/item?id=33223080

カスタムCSSを挿入して、すべてのアイテムや特定のアイテムを選択し、contain: contentcontent-visibility: auto; ルールを適用するブックマークレットがあれば、トリックになるかも。さらに、擬似セレクタの :empty { display: none; } を使うと、さらに効果があるかもね。

本当にそう思う。ウェブページに対して、5秒間100%のCPUやGPUを使うっていう制限を設けるのは、結構理にかなってると思う。(例えば、20秒間で25%とか。)それ以上は、オープンしている間ずっと3%のCPUに制限される感じで。人気のブラウザのデフォルトにして、サイトが効率的に作らざるを得ないようにすればいいんじゃないかな。WebGPUのデモやゲームみたいなものが、無制限の処理を要求できる許可もあればいいし。

ChromeOSには、バックグラウンドタブのCPU使用率を1%に制限するロジックがあると思う。

ユーザーがウェブデザイナーにバッテリーを無駄にすることで電気ショックを与えられるような仕組みを入れるべきだよ。

うん、StopTheMadnessがあるよ。https://underpassapp.com/StopTheMadness/ これはSafari、Chrome、Firefoxで動くけど、買わなきゃいけないよ。

そのサイトを使わないっていうのはどう?

固定高さのコンテナでラップして「overflow: hidden」を設定すればいいんだ。そうすれば、レイアウトエンジンはそのラッパーの外にある要素の位置を再計算する必要がないから、ずっと速くなるよ。ちなみに、昔はこのトリックで大きなレンダリングを速くしてたんだ。行や列のサイズをあらかじめ知っていればいいけど、それってちょっと目的に反するよね。

ありがとう!これがHNのコメントで探してたものだよ。記事によると、レイアウトエンジンがそんなにバカなわけないよね。十分なヒントを与えれば、そうでもないと思う。

これが答えだと思う。これは結構一般的な知識だと思ってたけど、高さのアニメーションは結構よくあるし(ドロップダウンとか考えてみて)、そんなに複雑にする必要はないよね。

フロントエンド開発者になろうとする以外に、こういう技術を基本から学ぶ良い方法ってあるのかな?

昔からのウェブ屋として、この記事みたいな修正を見ると悲しくなる。最近のフロントエンドの人たちはDOMに対するリスペクトが全然ないし、古い手法で作られたマクマスター・カーのサイトがどうしてあんなに良いのか不思議がってる。

これ、数年前にはもっと効果的だったかもしれないけど、今は特に重要じゃないね。高さを変えると要素が移動するだけで、ブラウザエンジンは位置が変わっても再レイアウトしないことが多いし。「overflow: clip」は「overflow: hidden」よりもずっと軽いよ。

これは便利な機能の巧妙な実装に見えるね。開発者がストリーミングオーディオAPIを使って便利なビジュアライザーにリンクさせたり、自分のアプローチの問題を認識して、ビジュアライザーのデバッグをしたり、マスキングみたいなグラフィックスの基本を使って巧妙な解決策を見つけたりするのが楽しかった。彼が修正が必要な機能を立ち上げたことで無責任だという一般的な合意にはあまり感心してないし、ユーザーがオーディオ接続についてフィードバックを求めていないとか、投稿者が`02年にもっと良いことをしたとか言われるのも納得いかないな。

この記事を読んだのは、先週CSSの高さトランジションを設定して、ラジオボタンの選択に基づいて条件付きコンテンツを表示/非表示にするフォームを作ったからなんだ。「これをやっちゃダメってことはないよね?」って思った。この記事のポイントは、CPUのパフォーマンスのために連続アニメーションは避けるべきだってことみたい。たまに使うトランジションには、そんなに影響はないと思うけど。「高さをアニメーションさせる人たち」のターゲット層にとって、この怖いタイトルはちょっと誤解を招くよね。

小さなCSSで脈打つ「ライブ」ボタンを作って、最悪のリソース消費を抑えるために脈打つ回数を固定したんだけど、それが正しいことだと気づくまでにちょっと時間がかかったよ!

代わりに、2つの長方形を使ってそれぞれにトランスレートを適用することで、高さが変わっているように見せることができるよ。これはすごく賢い解決策で、エンジニアには拍手を送りたいけど、これが解決策だっていうのが、ウェブUIの業界がどこにいるのか本当に絶望的に感じさせる。こういう直感に反する恐ろしいことがあっても、HTMLとCSSが勝ったっていうのがね。UIレイヤーは、意図を反映して気持ちよく感じさせてくれる。画像を取って、その画像を暗くするコードを書いて、最終的なユーザーに見せることができる。それは理にかなってる。でも、HTML+CSSでは、もう一つの要素、別の長方形を追加して、それを前に置いて、真っ黒に塗って、透明度を低く設定しなきゃいけない。確かに同じように動くけど、概念的にすごく醜い感じがするんだよね。

本をめくるアニメーションを試みたときに似たような問題があった。半分のところで、裏側のテキストを更新して、裏のページも更新しなきゃいけなかった。でも、何をやっても、ページがちょうど半分のところで更新がうまく同期できなかった。解決策はあるかもしれないけど、結局諦めちゃった。

これ、20年前のスライディングドアテクニックそのものだよね。