HTML
CSS
JavaScript
performance

CSS: will-change指定時の挙動, パフォーマンスへの影響と考察

More than 1 year has passed since last update.

※will-changeは編集時点(2016/07/27)でCandidate Recommendationの状態です。仕様変更による挙動差異などがあれば適宜コメントを頂けるとありがたいです。

TL;DR

  • 使いどころ
    • JavaScriptでアニメーションを行う際
    • CSS animation, transitionを:hover等で遅延実行する際
    • などの、初期値として変形やアニメーションの値を持たない要素を特定の要因で実行する際に効果を発揮する
  • ユースケースによって、常に指定するか、動かす直前から指定して終了時に解除するかを判断する
  • 最適化はUAごとに異なっており、かつ指定したからといって必ず恩恵が得られるものではない
  • "おまじない"的な使い方も有りだが、本当にパフォーマンスが必要な場面で有効かどうかはブラウザの実装を調べるのがてっとり早い

前提知識

Spec

Specがかなり分かりやすく書かれているので、上から下まで一読することをお勧めする。

個人的に分かりづらい、と思った点に関しては考察に合わせて記す。

ブラウザの実装調査

一先ず、ソースが調べやすいChrome, Firefoxで調査した。

また、以下の内容では特にパフォーマンスに関連する項目のみを記載する。例えば、will-changeの値としてStacking Contextを生成するようなプロパティを指定した際は、その要素はStacking Contextを生成しなければならないが、そういった実装箇所に関しては記載しない。

Chrome

  1. will-changeへの指定値をStyleWillChangeDataとして保持し、各実装から参照される
  2. transform, box offsets(top, left, bottom, right), opacityが指定された際に、Composited Layerが生成される
  3. contentsが指定された際に、その要素とその要素以下すべての要素にSubtreeWillChangeContentsというフラグがたち、その要素群の中のいずれかの要素がアニメーションしている場合に、その要素に対して
    • Composited Layerが生成される
    • Squashing(複数のRender Layerを1つのGraphic Layerへまとめること)が行われなくなる
  4. transformが指定された際に、ラスタライズ済みのレイヤのスケールの再計算を行わなくなる
    • この実装はPictureLayerのみ。何となくやってることは分かったが、ちょっと説明する自信が無いので詳しい方がいればご教示いただきたい

References

  1. StyleWillChangeData.h
  2. ComputedStyle.cpp#909, CompositingReasonFinder.cpp#89
  3. ComputedStyle.h#1049, CompositingReasonFinder.cpp#86
  4. picture_layer_impl.cc#926, picture_layer_impl.cc#432

Firefox

  1. will-changeへの指定値をbit fieldとして保持し、各実装からはビット演算で参照される
  2. transform, opacityが指定された際に、Composited Layer生成される
  3. scroll-positionが指定された際に、その要素が持つスクロールを行う(可能性がある)扱いになり、Composited Layerが生成される

ほか気になった点として、nsDisplayListBuilder::AddToWillChangeBudgetというメソッドで、will-changeが指定された要素をComposited Layerにするためのコストが規程の範囲内に収まるかどうかを判定していた。
この判定がfalseで返る場合、Composited Layerは生成されない。

References

  1. nsRuleNode.cpp#L6216-L6263, nsRuleNode.cpp#L5300-L5317
  2. ActiveLayerTracker.cpp#L424-L434
  3. nsGfxScrollFrame.cpp#L4836-L4861

検証

上記の実装を踏まえつつ、will-changeへの各値を指定した際の挙動をざっくりと確認するためのサンプルを用意した。
サンプルでは、will-changeを指定していない要素, 指定した要素の2つを並べ、それぞれの要素に対してwill-changeへ指定したプロパティをJavaScriptでアニメーションさせている。(contents指定のサンプルに関しては、内包するテキストを追加するようにした)

Timeline Profilingの実行結果, Composited Layerの有無, ほか気になった点に関して各ブラウザで確認した結果を合わせて記載するが、詳細な結果は各自Dev Tools等を用いて確認いただきたい。

scroll-position

http://output.jsbin.com/rumafo

browser Timeline Profiling Composited Layer
Chrome ほぼ同一 生成なし
Firefox 指定なし要素は、動き始めにfpsの落ち込みがある 指定した要素は常に有り, 指定なし要素はアニメーション時に有り

contents

http://output.jsbin.com/jenutac

browser Timeline Profiling Composited Layer
Chrome ほぼ同一 生成なし
Firefox ほぼ同一 生成なし

transform(2D)

http://output.jsbin.com/pojaqa

browser Timeline Profiling Composited Layer
Chrome 指定した要素はRasterizerの実行頻度が激減 指定した要素は常に有り
Firefox 指定なし要素は、動き始めにfpsの落ち込みがある 指定した要素は常に有り, 指定なし要素はアニメーション時に有り

transform(3D)

http://output.jsbin.com/hayuler

browser Timeline Profiling Composited Layer
Chrome 双方ともRasterizerの実行頻度が激減
指定なし要素は、動き始めにfpsの落ち込みがある
指定した要素は常に有り, 指定なし要素はアニメーション時に有り
Firefox 指定なし要素は、動き始めにfpsの落ち込みがある 指定した要素は常に有り, 指定なし要素はアニメーション時に有り

left

http://output.jsbin.com/manazer

browser Timeline Profiling Composited Layer
Chrome 指定した要素はRasterizerの実行頻度が激減 指定した要素は常に有り
Firefox ほぼ同一 双方ともアニメーション時に有り

opacity

http://output.jsbin.com/wiyuhan

browser Timeline Profiling Composited Layer
Chrome 指定した要素はRasterizerの実行頻度が激減 指定した要素は常に有り
Firefox ほぼ同一 指定した要素は常に有り, 指定なし要素はアニメーション時に有り

各検証には以下のブラウザを使用した。

  • Mac Google Chrome 52.0.2743.82 (64-bit)
  • Mac Mozilla Firefox 47.0.1
  • Mac Safari 9.1.2 (11601.7.7)

※調べたソースとまったく同一のバージョンでない点に注意(手元でビルドまではしていないので)
ここにも書いたが、Edgeはあまりプロファイラが役に立たなそうなので調べていない。詳しい方にご教示いただきたい。

考察

変化をさせる要素の、変化をさせるCSSプロパティを確実に指定し、その副作用を必ず留意する

CSS Will Change Module Level 1#dont-global

will-changeは変化させないCSSプロパティを指定しても何も意味が無い。

また、値として指定したCSSプロパティによっては、そのCSSプロパティ自体がその要素に指定されていなくともStacking Context, Containing Blockを発生させる点にも注意する必要がある。

常に指定し続けるのではなく、要素を変化させる前に猶予を持って指定し、変化終了後に指定を外す

CSS Will Change Module Level 1#dont-waste

前述したブラウザ実装で考えると、will-changeを指定した際にComposited Layerが生成されるため、そのレイヤを生成する, また保持する分だけのマシンリソースをさらに消費する。変化のない要素に対してwill-changeを指定すると言うことは、そのリソースを常に無駄にし続けることになる。
特にモバイル端末では、豊富なリソースを確保できない場合もあるため、こういった省力化を行うべきである。

ただし、その都度will-changeの指定を行うことは実装において煩雑さが増えるため、以下のようなルールでwill-changeを指定すればよいと考えている。

  • スプラッシュ画面、ページ表示時に1度しか動かないようなものは、動作開始時、終了時にwill-changeの指定値を切り替える
  • ページ表示中、常に動き続ける、メニューなどで頻繁に動くようなものは、CSS上に予め指定する
    • ただし後述の通り、指定した瞬間から最適化が実行されるようなCSSプロパティが要素に指定されている場合は、will-changeの指定自体が不要になりうる

ただ、どちらにせよ動的なスタイル指定というのは悩ましい部分はあり、Specに記載されている例で言うと

.element { transition: opacity .2s; opacity: 1; }
.container:hover > .element { will-change: opacity; }
.element:hover { opacity: .3; }

は正しいが、仮に.containerが特殊なレイアウトをしている場合に:hoverの範囲から外れ、.elementwill-changeの効力を失うということもあり得る。

JavaScriptで考えてみると

element.style.willChange = "opacity";
requestAnimationFrame(() => {
  element.style.opacity = 0;
  element.style.willChange = null;
});

のようなに、will-change指定後にrequestAnimationFrameを実行し最適化が終わっていることを見越してからアニメーションを開始するような実装になりそうだ。

ブラウザによっては、一部のCSSプロパティを指定した瞬間から最適化するものがあるため、そのような要素に対してさらに同じプロパティを値としたwill-changeを指定するのは、"現状では"必要はなさそう

例えば、Webkit系のブラウザでよく言われるtransform: translate3d(0, 0, 0);を指定するとComposited Layerを生成が生成されるため要素を動かした際にガクガクしなくなる、などのトリックがこれにあたる。
このような要素に対して、さらにwill-change: transform;を指定することは意味がない。

"現状では"と書いたが、今後will-change指定時だけ働くような最適化が生まれた場合はこの限りでは無い。


will-change難しい…、というか扱いづらい。
が、用法用量を守れば確実に効果があり、今までのようなトリックで暗黙的なComposited Layerを生成して誤魔化さなくても良くなる点に関しては良いかもしれない。