ブラウザで滑らかなアニメーションと効率的なレンダリング
ブラウザで滑らかなアニメーションを実現するための知識、TIPSをまとめました。
アニメーションの基礎知識
フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説!
ブラウザのアニメーションは、動画と同様に1コマ(静止画)を高速で切り替えることで表現されます。この1コマをフレームと呼び、1秒間に表示されるフレームの数をフレームレート(FPS) と呼びます。ブラウザ上で滑らかなアニメーションを作り出すためには、1秒間に60フレーム(60FPS)のレンダリングを行う必要があります。これは、1フレームのレンダリングを16.6ms(1000ms/60フレーム)以内に完了するということです。逆に言えば、16.6ms以内にアニメーションが完了しないと、画面がチラついたり、カクついて表示されます。
アニメーションとレンダリングの仕組み
では、1フレームが描画される際にどのような処理が行われているのかを詳しく見ていきます。
ブラウザは1フレームを描画するのに以下の工程を連続的に実行することで、Webページにコンテンツを表示しています。
1. Style
DOM要素に対して、どのようなCSSプロパティを適用するかを計算してスタイル情報を決定します。
2. Layout
DOM要素のサイズ、位置、マージン、パディングなどを計算します。
3. Paint
DOM要素がどのように見えるべきか(色、テクスチャ、フォントなど)という描画情報を生成します。その後、CSSの規則(親子関係やz-index等)に基づいて、DOM要素の重なり順(レイヤー)を考慮して描画順序も決定します。
4. Composite
レイヤーを組み合わせて(合成して)ページを作成し、ブラウザに描画します。
繰り返しになりますが、これらの工程は連続して行われます。つまり、Layoutを変更するアニメーションを実行した場合、PaintとCompositeの工程も再度実行されます。そのため、Compositeのみを変更するアニメーションに比べてレンダリングコストが高くなります。重要なポイントは、アニメーションによる変更(ex. 幅や高さ、色の変更など)が、どの工程に影響を及ぼすかを理解することです。
アニメーションとレンダリングパターン
Styleの工程では、DOMツリーの構造やDOM要素の属性に基づき、各要素に適用するスタイル情報を計算します。これは、DOMツリー構造の変化やDOM要素の属性の変更時にも行われます。
// Styleを引き越す処理の一例
const element = document.getElementById('content')
element.textContent = 'Change DOM'
element.classList.add('active')
1. Style --> Composite
Compositeだけで完結するCSSプロパティの変更は、レンダリングパフォーマンスが最も高いです。具体的には、次の2つのCSSプロパティが該当します。
- transform
- opacity
まず、求めているアニメーションをtransformやopacityを用いて実現できるかどうか検討してみてください。ユースケースのサンプルは以下のとおりです。
ユースケース1: 要素の移動
.element {
transition: transform 0.5s ease-in;
}
.animate {
transform: translateX(200px);
transition: transform 0.5s ease-in;
}
ユースケース2: 要素のサイズ変更
.element {
transition: transform 0.5s ease-in;
}
.animate {
transform: translateX(50px) scale(2, 1);
transition: transform 0.5s ease-in;
}
ユースケース3: 要素の表示、非表示
.element {
transition: opacity 0.2s ease-in;
}
.animate {
opacity: 0;
transition: opacity 0.2s ease-in;
}
2. Style --> Paint --> Composite
Layoutの処理をスキップすることにより、処理時間が短縮されます。しかし、Paintはメインスレッドで動作するため、パフォーマンスが低下する可能性があります。例えば、色の変更を行う際にはcolorやbackground-colorではなく、可能であればopacityを使用して実現できない検討してみてください。代表的なCSSプロパティの変更については以下のとおりです。
- color
- border-color
- background-color
- background-image
- z-index
3. Style --> Layout --> Paint --> Composite
すべての工程を再計算する必要があるため、パフォーマンスが低下する可能性があります。代表的なCSSプロパティの変更については以下のとおりです。
- width
- height
- margin
- padding
- display
以下にサンプルコードを掲載していますが、可能な限りtransformやopacityを用いて実現できないか検討してみてください。
.element {
left: 0;
transition: left 0.5s ease-in;
}
.animate {
left: 200px;
transition: left 0.5s ease-in;
}
.element {
width: 100px;
transition: width 0.5s ease-in;
}
.animate {
width: 200px;
transition: width 0.5s ease-in;
}
レイヤーの仕組みとパフォーマンス最適化
Compositeの工程で各レイヤーを合成すると説明しました。互いに独立したレイヤーにDOM要素を配置することで、レンダリングを効率的に行うことが可能です。以下のCSSプロパティを用いてアニメーションを実装すると、新しいレイヤーが作成されます。
- transform
- opacity
- will-change
transform、opacityのアニメーションを実装したことで、新たにelement、element2レイヤーが生成されている(Chromeの開発者ツールのLayersパネルで確認可能)。
レイヤーを分割することで、DOM要素に変更が生じた場合でもその変更が影響を及ぼすレイヤーだけを再計算すれば良いので、計算量を大幅に削減でき、パフォーマンスが向上します。
will-changeによるパフォーマンス最適化
will-changeは、特定のDOM要素が近い将来に変更されることをブラウザに通知するCSSプロパティです。つまり、ブラウザはDOM要素の変更が必要となる前に最適化を行うことで、レンダリングを高速化することができます。指定可能なプロパティ値は以下の通りです。
-
auto
デフォルト値です。ブラウザが自動的に最適なwill-changeの値を選択します。 -
transform, opacity, top, left, bottom, right
指定したCSSプロパティのアニメーション化、または変更されることをブラウザに通知することで、パフォーマンスが最適化される可能性があります。 -
contents
ブラウザが頻繁に変更されるDOM要素のレンダリング結果のキャッシュを減らすことで、ブラウザのリソースの無駄を抑える可能性があります。 -
scroll-position
DOM要素のスクロール位置が頻繁に変更されることをブラウザに通知することで、スクロールパフォーマンスを最適化する可能性があります。
will-changeを全ての要素に常に指定するのは避けてください。ブラウザのリソースを無駄に消費し、パフォーマンスに悪影響を及ぼす可能性があります。特定の要素がclickされた際のアニメーションを最適化するためのサンプルコードを以下に示します。
.element {
transition: transform 1s ease-out;
}
/* アニメーションを実行する予定の要素に対して、アニメーション開始直前に最適化をブラウザに通知 */
.element:hover {
will-change: transform;
}
/* 要素がclickされたらアニメーションを実行 */
.element.animate {
transform: rotateY(180deg);
}
ここでの注意点は、will-changeを適用するタイミングです。要素に対して予めwill-changeを指定するのではなく、アニメーションが実行される直前(この例ではhoverされたとき)に指定することで、ブラウザのリソースを効率的に活用します。
will-changeの効果を最大化するためには、必要なタイミングで適用し、不要になったら削除するという適切な管理が求められます。そのため、JavaScriptを使用して動的に操作することが推奨されます。以下に、MDNのコードサンプルを示します。
var el = document.getElementById("element");
// 要素がホバーされたとき、will-change を設定する
el.addEventListener("mouseenter", hintBrowser);
el.addEventListener("animationEnd", removeHint);
function hintBrowser() {
// アニメーションのキーフレームブロックで
// 変更されるであろう最適化可能なプロパティ
this.style.willChange = "transform, opacity";
}
function removeHint() {
this.style.willChange = "auto";
}
このコードでは、要素がマウスオーバーされたときにwill-changeプロパティを設定し、アニメーションが終了したときにリセットしています。これにより、パフォーマンスを効率的に向上させることができます。
Discussion