こんにちは、CSSとVue.jsでアニメーション使いまくりのポートフォリオ作ったり、シューティングゲーム作ったりして遊んでいるゆきです。
今日はCSSアニメーションで無茶しすぎてMacBookがカイロになった反省からの「負荷をかけずにぬるぬるのCSSアニメーションを実現するための試行錯誤」の顛末をまとめます。それでもCSSでアニメーションしたいんだ
今回の目的とサンプルケース
この記事では、WebでCSSを使ってゲームやアート的な表現にゴリゴリのアニメーションを使いたい!というケースを想定します。
全体を通してCSSのwill-changeプロパティを使ったGPUレンダリングによる最適化のお話です。will-changeってなに?って方はこの後でてくる参考記事リストを先に見ていただくのが良いと思います。
https://css-anime.firebaseapp.com/
今回検証するアニメーションがこちらです:
- CSSで描いた花が1000個、順次開花する
- 個々の花はシンプルなdiv要素(今回は画像やSVGは使っていません)
- 新しい花は32msごとに回転・拡大しながら現れる
- 開花後の花はそのまま残る
- 上記に加え、画面全体も回転し続ける
一般化すると、アニメーションしながら要素が段々増えていく
+ 常時全てが動いているわけではないけれど画面からは消せない
という結構辛いシチュエーションです。
実験環境は下記の通り
- MacBookPro 2017Late / 8GB RAM(よく「梅」って言われるProの一番下のスペック)
- MacOSX Mojave
- Chrome 74
SafariやiOSについては後ろの方で少しだけ言及します。
環境による差異の大きい部分なので、ブラウザやOSが変わると大きく結果が変わってくる可能性があることはご了承ください。
神々の叡智:一緒に読んでおくと良いかもしれないもの
今回の試行錯誤で参照したありがたい記事のリストです。
- CSS Will Change Module Level 1(日本語訳)
- CSS: will-change指定時の挙動, パフォーマンスへの影響と考察
- ハイパフォーマンスCSS3アニメーション——60fpsを実現するベストプラクティス
- コンポジタ専用プロパティの優先使用、およびレイヤー数の管理
実験0: 基本となる要素とアニメーションの構築
まずは手始めに普通に書いてみます。どんなものを動かしているのか示すためにソースを載せていますが、流し見していただければ十分かと思います。
お花のhtml部分はこんな感じ。楽するために&個人的趣味でVue.jsを使っていますが1フレームワークはなんでも良いです。
<div class="flower-root"
:class="{animate: visible}"
@animationend="onEndAnim">
<!-- 花びら -->
<div class="petal" v-for="(petal, index) in petals" :key="index"
:style="{
transform: `rotate(${petal.r}deg)`,
'background-color': petal.col
}">
</div>
<!-- まんなか -->
<div class="center"></div>
</div>
花びらの枚数だけdivを並べます。角度と色だけ動的にテンプレート内で指定して他のスタイルは以下の通り<style>
で一律に定義します。開花のアニメーションもここで指定します(つまり、アニメーションそのものはVueもJavaScriptも関係のないただのCSSアニメーションです)。
<style lang="scss" scoped>
.flower-root {
position: absolute;
transform: rotate(0deg) scale(0);
animation: rotate 2s ease-out 0s 1 normal forwards;
}
.petal {
position: absolute;
width: 70px;
height: 20px;
top: -10px;
left: 0;
transform-origin: left center;
border-radius: 50px;
background-color: #ffb7aa;
}
.center {
position: absolute;
width: 30px;
height: 30px;
left: -15px;
top: -15px;
border-radius: 30px;
background-color: #ffe683;
}
// 回転・拡大しながら現れるアニメーション
@keyframes rotate {
0% { transform: rotate(0deg) scale(0); }
100% { transform: rotate(360deg) scale(1); }
}
</style>
いささかしょぼいですが、こんな感じでdivだけでお花ができました。
このFlower
コンポーネントを一定時間ごとに画面に追加しつつ、画面全体をぐるぐる回します。
実験1: そのまま実行(will-changeなし)
まずはwill-changeをつけずにこのままアニメーションを実行してみます。Chromeのパフォーマンス表示に加えて、アクティビティモニタで左側にCPUとGPUの利用率を表示しています。
CPUが結構頑張ってますが、60fpsをキープしてます。意外といけるか?
500個。400個前後でCPUが限界を迎えた模様。フレームレートが一気に落ちていきます。ブラウザが気を利かせてGPUに助けを求めているようですがフレームレートは安定しません。

終盤になると、平均フレームレートは20fps前後まで落ち込む結果に。ファンが回りだしてバッテリーがガンガン減っていきます。
すべての開花が終わって、ただ静止した1000個の花が回っているだけになりました。ここまでくるとようやく60fpsに復帰します。
実験1の結論
- CSSアニメーションは対象要素数に比例してCPU負荷が上がる
- CPUが限界になると一気にフレームレートが落ちる
実験2: じゃあもう全部will-changeつけちゃおう
教科書的にはやるべきではない誤った最適化
とされているものです。でもまあ、試してみないことにはわからないのでやってみましょう。変更点はスタイルではじめっからwill-change: transform
をつけるだけです。
.flower-root {
position: absolute;
transform: rotate(0deg) scale(0);
animation: rotate 2s ease-out 0s 1 normal forwards;
will-change: transform; // 追加
}
レッツ・スタート!

500個。苦しさが見えてきました。GPUの負荷がうなぎのぼりでCPUにも負荷がかかってます。

終盤にはCPUもGPUも悲鳴をあげています。パフォーマンス以前にクラッシュの危険がある状態。
すべての開花アニメーションが終わっても負荷は一向に下がりません。Will-changeをつけるということはいつ動いてもいいように最適化の維持を命じることなので、アニメーションが終わったとしても(終わったと私たちは理解していても)負荷は下がらない、ということですね。
教科書的に書いてあることそのままですが、思っていた以上に影響が大きいです。
実験2の結論
- will-changeをつけることでGPUを働かせることはできる
- will-changeをつける要素が多いとGPUとCPUの両方に負荷がかかる
- will-changeをつけたままだとアニメーションが終わっても、負荷は下がらない
実験3: アニメーションが終わったらwill-changeを取る
実験2の問題は「開花アニメーションはすぐ終わるのに、終わった後もwill-changeをつけっぱなしにしたこと」でした。よって実験3では、開花アニメーションが終わった時点でwill-changeを外します。
will-changeを動的に変更するために、プロパティをテンプレート内に移動して変数でON/OFFできるようにします(ここではvue側に自分で定義したisMoving
変数に連動させています)。アニメーションの終了はanimationend
で拾えるため、このイベントハンドラでスイッチをOFFに切り替えます。
<div class="flower-root"
:style="{
'will-change': isMoving ? 'transform' : 'auto',
}"
@animationend="onEndAnim">
...
private onEndAnim() {
this.isMoving = false
}
前置き長くなりましたが、アニメーションの終了を検知してwill-changeを初期値のauto
に変更しているだけですね。
では、スタート!
500個。あれ?CPUが高いです。フレームレートも300個超えたあたりから落ちだしました。アニメーションが終わったものからwill-changeを外してGPU処理から下ろしているので、GPU側は軽快です。でもCPUがこれだと...

すべてのアニメーションが終わって、ようやく60fpsに復帰しました。
何がおきているのか?
おかしいです。明らかに期待しない結果になってしまいました。
教科書通りに「アニメーションが終わったら速やかにwill-changeを外す」を実践しただけです。何が起きているのでしょうか?

Chromeのパフォーマンスモニタでキャプチャをとってみると、Update Layer Tree
という謎の処理が一定時間ごとに走っています。
Chrome内の処理なのであまり具体的なことはわからなかったのですが、
DevToolsのTimelineパネルを見ながら、レンダリングの仕組みを理解する
Update Layer Tree
GPUが処理を行うレイヤを更新しています。
ということで、GPUが処理を行うために対象の要素をレイヤーに昇格したり、不要になったレイヤーを通常の描画に降格させるための再構成を行なっているようです。
今回試行錯誤した結果、どうやら現行のChromeでは以下の傾向があるようです
- 昇格/降格する処理対象の要素数ではなく、全体のレイヤー数に依存してUpdate Layer Treeが重くなる
- レイヤーへの昇格よりも降格の方が重い。つまり、will-changeを外す処理は重い
ソースは読めていないので、これが正しい挙動なのかはわかりません...
実験3の結論
- GPUの負荷を抑える観点でアニメーションが終わったらwill-changeを外すという原則は正しい
- GPUの負荷が高い状態ではwill-changeをつけたり外したりする処理は重い。特に外す方が重くなる
- 高負荷状態でアニメーションが終わったからといってパラパラとwill-changeを外すのは致命的
実験4: ある程度まとめてwill-changeを外す
実験3で、アニメーションが終わったからといってバラバラにwill-changeを外すのはヤバい、ということがわかりました。なんかもうこの時点でChromeにはがっかりだよ!という気持ちなのですが2、なんとか回避策を探します。
実験4では、「1個will-change外すだけでUpdate Layer Treeが実行されて処理が重くなる」なら「1個でも100個でも同じじゃね?」の発想でアニメーションが終わった要素のwill-changeを一定個数まとめて外してみます。
Flowerインスタンスを格納する配列を用意して
const queueLimit = 100
const stopedFlowers: Flower[] = []
アニメーション終了時に配列に追加して、100個溜まったらisMoving = false
に設定してwill-changeをはずします。
private onEndAnim() {
stopedFlowers.push(this)
if (stopedFlowers.length === queueLimit) {
stopedFlowers.forEach(fl => fl.isMoving = false)
stopedFlowers.length = 0
}
}
なんだか汚いやり方だなぁ...と思いつつ。実験スタート!

100個目。まだ100個目のアニメーションが始まったところなのでwill-changeは全部付けっ放し。つまり、実験2と同じ状態ですね。

500個目。100個単位でwill-changeを外しているので、定期的に一瞬だけフレームレートが落ち込みます。
終盤までこの傾向は変わらず。一瞬カクッとフレームレートが落ちるのが悲しいですが、平均的な負荷は安定しています。
無事完走。最後の100個の解放が終わった時点ですべてwill-changeが解除され、実験1,3と同じ状態になりました。
実験4の結論
- will-changeはアニメーション終了後にある程度まとめて解除するのがよい
- will-change解除時の負荷は(今回の実験の範囲では)完全には回避できない
- 重いアニメーションでは「隙を見てwill-changeを外す」ような工夫がアプリ側に必要
ちなみにSafariではどうなの?(オチ)
ここまで試行錯誤しておいてあれなのですが、MacOS/iOSのSafariでは実験3でヌルサクです。
...つまりこれはChromeのバグないしは仕様上の問題なのでは?と思うのですが確定的なソースを見つけられず。
詳しい方いらっしゃったらコメントをいただけると嬉しいです。
まとめ
- GPUを使ってぬるぬるCSSアニメーションしたいならwill-changeを設定しよう
- will-changeはアニメーションが終わったらちゃんと解除しよう
- Chromeではwill-changeの解除はUpdate Layer Treeによるレイヤーの再構成を起こして重くなる。 アプリ側で「隙を見てまとめてwill-changeを外す」ことができれば影響を最低限にできる
- パフォーマンスのチェックは複数ブラウザ・複数環境で。Chromeが正義だと思い込まないこと
- アクティビティモニタ等、OS側のモニター機能もチェックしよう。60fps出ていてもマシンは悲鳴をあげてるかも?