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

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

## TL;DR

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


## 前提知識

- Webブラウザのレンダリングの仕組み
    - [ブラウザのしくみ: 最新ウェブブラウザの内部構造 - HTML5 Rocks](http://www.html5rocks.com/ja/tutorials/internals/howbrowserswork/#Render_tree_construction) (だいぶ古いので概念として)
    - [GPU Accelerated Compositing in Chrome - The Chromium Projects](https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome)
    - [Slimming Paint (a.k.a. Redesigning Painting and Compositing) - The Chromium Projects](https://www.chromium.org/blink/slimming-paint)
- 合成レイヤーとその確認方法
    - [Accelerated Rendering in Chrome: The Layer Model - HTML5 Rocks](http://www.html5rocks.com/ja/tutorials/speed/layers/)
    - ほか、"ブラウザ 合成レイヤー"とか"ブラウザ GPU"とかでググる
- 描画周りのプロファイリング、Dev Tools
    - [ブラウザの描画関連プロファイリングツールと利用方法 - Qiita](http://qiita.com/damele0n/items/363d6e30b92ed0c75b02)


## Spec

- [CSS Will Change Module Level 1](https://drafts.csswg.org/css-will-change/)
- [CSS Will Change Module Level 1 （日本語訳）](https://triple-underscore.github.io/css-will-change-ja.html)

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

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


## ブラウザの実装調査

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

- Google Chrome 53.0.2785.16
- Firefox 49
    - https://hg.mozilla.org/mozilla-central/ でソースが見づらかったので"FIREFOX_AURORA_49_BASE"のタグが切られたcommit hashをもとに[github](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0)で参照した
    - 実装の詳細に関しては https://dxr.mozilla.org/mozilla-central/ で調べた

また、以下の内容では特にパフォーマンスに関連する項目のみを記載する。例えば、`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](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/third_party/WebKit/Source/core/style/StyleWillChangeData.h)
2. [ComputedStyle.cpp#909](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/third_party/WebKit/Source/core/style/ComputedStyle.cpp#909), [CompositingReasonFinder.cpp#89](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/third_party/WebKit/Source/core/layout/compositing/CompositingReasonFinder.cpp#89)
3. [ComputedStyle.h#1049](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/third_party/WebKit/Source/core/style/ComputedStyle.h#1049), [CompositingReasonFinder.cpp#86](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/third_party/WebKit/Source/core/layout/compositing/CompositingReasonFinder.cpp#86)
4. [picture_layer_impl.cc#926](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/cc/layers/picture_layer_impl.cc#926), [picture_layer_impl.cc#432](https://chromium.googlesource.com/chromium/src/+/53.0.2785.16/cc/layers/picture_layer_impl.cc#432)


### Firefox

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

ほか気になった点として、[`nsDisplayListBuilder::AddToWillChangeBudget`](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0/layout/base/nsDisplayList.cpp#L1374-L1404)というメソッドで、`will-change`が指定された要素をComposited Layerにするためのコストが規程の範囲内に収まるかどうかを判定していた。
この判定が`false`で返る場合、Composited Layerは生成されない。

#### References

1. [nsRuleNode.cpp#L6216-L6263](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0/layout/style/nsRuleNode.cpp#6216-6263), [nsRuleNode.cpp#L5300-L5317](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0/layout/style/nsRuleNode.cpp#5300-5317)
2. [ActiveLayerTracker.cpp#L424-L434](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0/layout/base/ActiveLayerTracker.cpp#L424-L434)
3. [nsGfxScrollFrame.cpp#L4836-L4861](https://github.com/mozilla/gecko-dev/blob/01efb84b2aa5b6fc26fbbbdedbb1d6a5c79025c0/layout/generic/nsGfxScrollFrame.cpp#L4836-L4861)


### 検証

上記の実装を踏まえつつ、`will-change`への各値を指定した際の挙動をざっくりと確認するためのサンプルを用意した。
サンプルでは、`will-change`を指定していない要素, 指定した要素の２つを並べ、それぞれの要素に対して`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の実行頻度が激減<br>指定なし要素は、動き始めに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)

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


### 考察

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

[CSS Will Change Module Level 1#dont-global](https://drafts.csswg.org/css-will-change/#dont-global)

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

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


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

[CSS Will Change Module Level 1#dont-waste](https://drafts.csswg.org/css-will-change/#dont-waste)

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

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

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

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

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

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

JavaScriptで考えてみると

```js
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を生成して誤魔化さなくても良くなる点に関しては良いかもしれない。