CSS will-changeプロパティについて知っておくべきこと

はじめに

WebKit系ブラウザでCSS transformやanimationといったプロパティを使った時に発生する、“例のちらつき”。これに気づいたことのある人ならば、おそらく“ハードウェア・アクセラレーション”という用語をこれまでにも耳にしたことがあるでしょう。

CPU, GPU, ハードウェア・アクセラレーション

一言で言うと、ハードウェア・アクセラレーションとは、グラフィックス・プロセッシング・ユニット(GPU)を用いてセントラル・プロセッシング・ユニット(CPU)の処理量を軽減し、ブラウザのレンダリング処理を効率化することです。ハードウェア・アクセラレーターを有効にしてCSS処理を使うと、ページのレンダリングが速くなり、ページ表示が高速化されます。

名前の通り、CPUとGPUはどちらもプロセッシング・ユニットです。CPUはコンピュータのマザーボードに取り付けられている部品で、ほとんど全ての処理・計算を行う、言わば“コンピュータの頭脳”です。一方、GPUはグラフィックカードに搭載されている部品で、画像の処理・レンダリングを担当します。さらに、画像表示に必要な、複雑な幾何学的計算処理も行います。つまり、GPUに処理をオフロードすることで、コンピュータのパフォーマンスを最大限に引き出し、モバイル端末におけるCPUの競合を軽減することができるのです。

ハードウェア・アクセラレーション(またの名をGPUアクセラレーション)によってページを表示させる場合、レイヤーという概念を用います。ページ上の要素に対し何らかの指示(例えば、3D transforms)を与えた時、その要素は自身の“レイヤー”に送られ、そのレイヤー内で、ページ上の他の要素とは独立してレンダリングされます。その後、その要素がページ内に合成されます(つまり画面上に描画される)。特定の要素の変形処理だけが唯一の変更点である場合、それ以外の要素まで再レンダリングする必要はありません。そこで、対象となる要素だけを取り出してレンダリングを行い、これにより描画処理の高速化を実現することができます。ただしこれは、3D transformsのみに当てはまることで、2D transformsには適用されません。

CSS animation、transform、transitionのGPUアクセラレーションは自動化されていません。これらは、ブラウザに搭載されている動作の遅いレンダリングエンジンによって実行されます。しかし、一部のブラウザでは、レンダリング処理能力を向上させるため、特定のプロパティを用いたハードウェア・アクセラレーションを実装しています。例えば、opacityです。このプロパティはGPUによって容易に処理されるため、正常に高速化することのできる数少ないプロパティの1つとなっています。CSS transitionやanimationにおいて透明度を変更したいレイヤーがある場合、ブラウザはそれをGPUに担当させ、そこで一切の処理を行わせます。このためレンダリングがとても速くなるのです。CSSのプロパティの中で、opacityは最も有用なツールです。このプロパティを使う上で大きな問題は発生しないでしょう。その他、ハードウェア・アクセラレーションが可能なプロパティとしては、CSS 3D transformsがよく使われます。

従来の方法:translateZ() (あるいはtranslate3d()) CSSハック

ずいぶん長い間、私たちは、translateZ() (あるいはtranslate3d())と呼ばれるCSSハック(null transformハックとも呼ばれる)を用いてレンダリングの高速化を行ってきました。これは、ブラウザをだまして無理やりanimationやtransformのハードウェア・アクセラレーションを行わせる方法です。3次元空間の中で変形させるわけでもない要素に単純な3D変形処理の指示を与えることでレンダリング処理を高速化します。例えば、2次元空間でアニメーションさせる要素に次のようなシンプルな指示を与えることで、ハードウェア・アクセラレーションを実行できます。

transform: translate3d(0, 0, 0);

こうした方法でハードウェア・アクセラレーションを行うと、合成レイヤーとして知られているものが生成されます。これは、GPUにアップロードされGPUによって合成されるレイヤーです。しかし、CSSハックを用いたレイヤー生成は、パフォーマンス・ボトルネックの解消に必ずしも役立ちません。レイヤーを作らせることでページ表示速度は速くなるかも知れませんが、コストがかかります。RAMやGPUのメモリ使用量が多くなり(特にモバイル端末では)、レイヤーをたくさん抱えると悪影響が及びます(特にモバイル端末では)。ですから、ハックを使うときは慎重に扱わなければなりません。ハードウェア・アクセラレーションの実行によって本当にページ表示が最適化されるのか、そしてその操作で新たなパフォーマンス・ボトルネックが起きていないことを確認した上で、これらのテクニックを使うようにしなければなりません。

レイヤー生成を促すCSSハックに代わるものとして、新たなCSSプロパティが導入されました。このプロパティは、要素に変更を加える前にそれがどのような変更であるかをブラウザに前もって知らせる機能を持っています。これにより、ブラウザはその要素の操作をあらかじめ最適化しておくことができ、例えばアニメーションのようなコストのかかる処理の準備をアニメーションが実際に始まる前にすることができます。これが、will-changeという新プロパティです。

新しい方法:素晴らしきwill-changeプロパティ

要素にどのような変更を加えるかを前もってブラウザに知らせるのがwill-changeプロパティの役割です。これを用いれば、その変更が行われる前に、適切な最適化をセットアップすることができます。つまり、ページ表示のレスポンスに悪影響を与え得る多大なコストを回避できるということです。その結果、要素の変更・レンダリング処理は速くなり、ページは一瞬で更新され、滑らかな画像処理が可能になります。

CSS 3D Transformsを例に取ってみましょう。先ほども言ったように、このプロパティをある特定の要素に対して用いると、その要素とそのコンテンツはレイヤーに送られ、後で1つに合成(つまり画面に描画)されます。しかし、新たなレイヤーに要素をセットアップするのは比較的コストのかかる作業であり、結果としてアニメーションに数分の1秒単位の目立った遅れを生じます。これが画像の“ちらつき”につながるのです。

この遅れを避けるには、要素の変更が実際に起きる前に、その変更についてブラウザに前もって知らせてあげればいいのです。そうすれば、ブラウザは余裕を持ってそれらの変更に備えることができます。変更が実際に起きる頃にはその要素のレイヤーが準備できており、アニメーションおよび要素のレンダリングは適切に処理され、ページは迅速に更新されることになります。

will-changeプロパティを使ってこれから行われる変形処理についてブラウザに知らせたい時は、対象となる要素に対して以下のような指示を与えるだけでいいんです。

will-change: transform;

変形処理以外にも、スクロール位置(表示されているウィンドウ内での要素の位置。画面内に要素がどれだけ見えているか)、コンテンツなど、その他複数のCSSプロパティを変更したい時は、その対象となるプロパティの名前を指定することでブラウザに変更の意志を宣言することができます。1つの要素の複数の値を変更するつもりであれば、カンマで区切ってそれらをまとめて記述することができます。例えば、ある要素をアニメーションさせると同時に動かしたい(位置を変更したい)場合は、ブラウザに次のように宣言すればいいのです。

will-change: animation, position;

何を変更したいのかを正確に記述することで、その変更に備えた最適化をブラウザがやってくれます。ハックを使って不要で無駄かもしれないレイヤー生成をブラウザに無理やり行わせるやり方よりも、この方法のほうが明らかに高速化に役立ちます。

will-changeは、要素の変更をブラウザに知らせる以外に、要素自体にも何か影響を与えるのか

答えは、イエスあるいはノー。変更するプロパティの種類によって状況が変わってきます。要素上にスタックコンテキストを生成するようなプロパティであれば、それをwill-changeで指定することで要素にスタックコンテキストが作成されます。

例えば、clip-pathとopacityプロパティはいずれも、デフォルト値以外の値を指定した時、対象となる要素にスタックコンテキストを生成します。つまり、will-changeの値として上記いずれかのプロパティ(あるいは両方)を使った場合、要素に対する変更が実際に起きる前にその要素にスタックコンテキストが作られるのです。同じことは、要素にスタックコンテキストを作成するその他のプロパティについても言えます。

また、一部のプロパティは、位置が固定された要素に対し、包含ブロックの生成を引き起こします。例えば、変形処理された要素は、位置が固定されているものであっても、全ての子孫要素に対し包含ブロックを作成します。つまり、包含ブロックの生成を引き起こすプロパティをwill-changeの値として指定した場合、位置が固定された要素に包含ブロックが作られるということです。

これらの例外をのぞけば、will-changeプロパティが、対象となる要素に直接的な影響を及ぼすことはありません。will-changeは、ブラウザにレンダリングを予告し、これから起きる変化に備えて最適化を行わせるだけです。上述したスタックコンテキストや包含ブロックを生じるような場合以外に、will-changeが要素に直接的な影響を与えることはありません。

will-changeの使い方:「すべきこと」と「してはいけないこと」

will-changeの役割が分かったところで、次のように考えたくなるものです。「ブラウザに全てを最適化させればいいのではないか」と。もっともな発想です。誰だって全ての変更に関して一気に最適化できたらと思うはずです。

確かにwill-changeはパワフルで素晴らしいツールですが、他のパワフルなツールについても言えるように、威力がある分、責任を持って扱わなければなりません。will-changeは賢く使うべきものであり、そうしなければパフォーマンス・ヒットが起きてページがクラッシュしてしまうでしょう。

will-changeは、パフォーマンス・ヒットをはじめ、すぐには検出できない副作用(そもそもwill-changeとは、見えないところでブラウザに話しかける方法なので、検出できないのは当然)を生じるため、扱いにくいプロパティです。これを使う時は、その力を最大限に活用し、誤用から生じる障害を避けるために、以下のことを頭に入れておいてください。

過剰な数のプロパティや要素に対し、will-changeを使って変更を宣言しないこと

先ほども述べた通り、全てのプロパティ、全ての要素に対して行う変更処理を全部ブラウザに最適化させようと考えたとしましょう。この場合、スタイルシートに次のような記述をします。一見、理にかなっているように見えますよね。

*,
*::before,
*::after {
    will-change: all;
}

これは見た目には有効に思えますが(私も最初は理にかなっていて、うまくいきそうだと思いました)、実は非常に有害で、何より全く有効ではありません。すべてのキーワードがwill-changeに対して無効な値であるだけでなく(有効な値と無効な値については、後ほど紹介します)、このように全てに適用されるルールは有用ではありません。というのも、ブラウザはすでにできる限りの最適化を行おうとしているので(opacityや3D transformsの例を思い出してください)、最適化を明確に指示しても何も変わらないし、何の役にも立たないからです。実のところ、この設定を行うことによって、多くの弊害が生じる恐れがあります。なぜなら、will-changeにひも付けされた強力な最適化がマシンのリソースを大量に消費する結果を招き、このように多用された場合はページのスピードの遅延や時にクラッシュまで引き起こす原因となってしまうからです。

つまり、起こるか起こらないか分からない変化に備えてブラウザを待機させるのは賢くないし、効果よりもむしろ害を与えてしまいます。ですから、やめてください。

ブラウザに十分な時間を与える

will-changeプロパティの名称の由来には、明確な理由があります。will-changeがブラウザに知らせるのは、将来起こる変化であり、今起こっている変化ではありません。will-changeを使って、これから宣言する変更に対して最適化を行うようブラウザに命じるわけですが、それを実現するためには、ブラウザにも最適化を行うための時間が必要です。これは、実際に変化が生じる時に遅延なく最適化を適用できるようにするためです。

変化が生じる直前の要素に対してwill-changeを設定しても、ほとんど効果はありません。(それどころか、設定しないよりも悪いかもしれません。以前はアニメーションに必要でなかった新しいレイヤーを作ってしまう可能性があります!)例として、ホバーで変化が生じる場合を想定します。

.element:hover {
    will-change: animation;
    animation: my-anim 2s linear infinite alternate;
}

上記のコードがブラウザに命じているのは、すでに起こっている変化に対する最適化です。これでは効果がないだけでなく、will-changeの包括的な概念を否定するようなものです。必要なのは、最低でも変化が生じるほんの少し前にその変化を予測する方法を見つけて、will-changeを設定することです。

例えば、要素がクリックされる時に変化するとしたら、その要素がホバーされる時にwill-changeを設定すれば、ブラウザが変化に備えて最適化する時間を稼げます。ユーザが要素にホバーしてから実際にクリックするまでの時間で、ブラウザは十分に最適化を行うことができるからです。人間の反応には比較的時間がかかるため、実際に変化が起こる前に約200ミリ秒の時間がブラウザに与えられることになり、ブラウザが最適化を行うのには、それだけの時間があれば十分です。

.element {
    /* style rules */
    transition: transform 1s ease-out;
}
.element:hover {
    will-change: transform;
}
.element:active {
    transform: rotateY(180deg);
}

では、クリックではなく、ホバーで変化させたい場合はどうでしょうか?すでに述べたように、上記の宣言は役に立ちません。しかしこの場合でも、変化が生じる前にそれを予測する何らかの方法を見つけることはできます。例えば、変化する要素の祖先にホバーすれば、リードタイムを稼ぐことができます。

.element {
    transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
    will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
    opacity: .5;
}

しかし、祖先にホバーしたからといって、対象の要素が必ずインタラクトされるとは言えません。したがって、アプリケーションでビューがアクティブになる時、もしくは要素がviewportの見える位置にある時にwill-changeを設定すれば、要素がインタラクトされる可能性は高まります。

変化が終了したらwill-changeを削除する

ブラウザがこれから起ころうとしている変化に対して最適化をすると、一般的にコストがかかります。先ほど述べたように、マシンのリソースを大量に消費することがあるからです。ブラウザは普通、適用した最適化を削除し、できるだけ早く通常の行動に戻ろうとします。しかし、will-changeはこの行動を無視し、本来ブラウザが行うよりもずっと長く最適化を維持してしまうのです。

よって、要素の変化が終了したら、必ずwill-changeを削除するようにしましょう。そうすれば、ブラウザは最適化のために使われているリソースを回復することができます。

スタイルシートで宣言されたwill-changeは削除できません。そのため、will-changeの設定や削除にはJavaScriptの使用が推奨されることがほとんどです。スクリプトでブラウザに変化を宣言し、その変化がいつ終了するかをリッスンすれば、変化が終了する後にwill-changeを削除できます。例えば、前のセクションで紹介したスタイルルールと同様に、要素(またはその祖先)がホバーされる時をリッスンして、mouseenterにwill-changeを設定することができます。要素をアニメーションする場合は、DOMイベントのanimationEndを使ってアニメーションが終了する時をリッスンし、animationEndが発生する時にwill-changeを削除します。

// Rough generic example
// Get the element that is going to be animated on click, for example
var el = document.getElementById('element');

// Set will-change when the element is hovered
el.addEventListener('mouseenter', hintBrowser);
el.addEventListener('animationEnd', removeHint);

function hintBrowser() {
    this.style.willChange = 'animation';
}

function removeHint() {
    this.style.willChange = 'auto';
}

クレイグ・バックラーが、JavaScriptでCSSのアニメーションイベントをキャプチャする方法について記事を書いています。この方法に精通していない方は参照してください。また、CSS-TRICKSのCSS animationとtransitionを制御するも参考になるでしょう。

スタイルシートではwill-changeを控えめに使う

前のセクションで見てきたように、will-changeは、要素に変化が生じる数ミリ秒前に、ブラウザにそのことを知らせる場合に使用できます。これは、スタイルシートでwill-changeを宣言してよい事例の1つです。will-changeの設定および削除にはJavaScriptを使うのがお勧めですが、なかには、will-changeをスタイルシートで設定(および維持)するのが適切な場合もあります。

一例として、ユーザが繰り返しインタラクトすることが想定され、かつ素早いレスポンスを求められる少数の要素に対してwill-changeを設定する場合があげられます。限定した数の要素に設定するのであれば、ブラウザによる過剰な最適化にはつながらないため、弊害も少なくて済みます。例えば、ユーザの要求に応じてサイドバーをスライドさせて変形する場合に適切なルールは次の通りです。

.sidebar {
    will-change: transform;
}

もう1つの例としては、ほぼ常に変化する要素に対してwill-changeを用いる場合です。例えば、ユーザのマウスの動きに反応する要素で、マウスの動きに合わせてスクリーンを移動するようなケースです。この場合は、スタイルシートでwill-changeの値を宣言して構いません。なぜなら、要素が常に、もしくは定期的に変化するため、最適化も維持されるべきだということを示しているからです。

.annoying-element-stuck-to-the-mouse-cursor {
    will-change: left, top;
}

will-changeプロパティの値

will-changeプロパティは、次の4つの設定可能な値、auto、scroll-position、contents、の1つをとります。

の値には、変化させたい1つ以上のプロパティの名前を指定します。プロパティが複数の場合はコンマで区切ります。次の例は、プロパティの名前が指定された有効なwill-changeの宣言です。

will-change: transform;
will-change: opacity;
will-change: top, left, bottom, right;

の値は、通常 から除外されるキーワードに加えて、次のキーワード、will-change、none、all、auto、scroll-position、contentsを除外します。この記事の最初で述べたように、will-change: allの宣言は無効なので、ブラウザに無視されます。

autoという値は特定の意志を示さないため、ブラウザは普段行う最適化以外の最適化を行いません。

scroll-positionの値は、その名が示す通り、近い将来、要素のスクロール位置を変更することを表します。この値を設定すると、ブラウザは、スクロール可能な要素を含むウィンドウに見えている以外のコンテンツのために前もって備え、レンダリングするため、便利です。ブラウザはスクロールウィンドウに見えているコンテンツだけをレンダリングすることが多いですが、なかにはウィンドウからはみ出ているコンテンツもあります。レンダリングを省くことで、メモリおよび時間の節約と、スクロールの見た目の良さのバランスをとっているのです。will-change: scroll-positionを使えば、さらなるレンダリングの最適化が実行され、より長い、または速いコンテンツ、もしくはその両方のコンテンツをスムーズにスクロールできます。

contentsの値は、要素のコンテンツの変化を表します。ブラウザは通常、要素のレンダリングを長い間キャッシュします。というのも、ほとんどの要素は頻繁に変化するわけではないし、変化しても位置を変える程度だからです。ブラウザがこの値から受け取るシグナルは、要素のキャッシュを控える、もしくはキャッシュを一切しないというものです。その理由は、要素のコンテンツが定期的に変化する場合、その要素をキャッシュしても役に立たないし、時間の無駄だからです。よって、ブラウザは単純にキャッシュをやめ、コンテンツが変化するたびに最初からレンダリングし続けます。

先にも述べましたが、will-changeを指定しても何の影響も受けないプロパティもあります。というのも、ブラウザはほとんどのプロパティの変化に対して、何ら特別な最適化を行わないからです。そうしたプロパティにwill-changeを指定しても安全ですが、ただし、影響はまったくありません。その他のプロパティでは、スタックコンテキスト(opacity、clip-pathなど)、または包含ブロック、もしくはその両方が生じるでしょう。

対応ブラウザ

この記事を書いている時点でwill-changeプロパティに対応しているブラウザは、Chrome Canary 36+、Opera Developer 23+、Firefox Nightlyだけですが、安定したビルドにリリースする意志もあります。また、すべてのモダンブラウザで対応になるのにそれほど時間はかからないだろうという話もされています。

まとめ

will-changeプロパティはハック不要な最適化コードを実現させ、CSS操作のスピードとパフォーマンスの重要性を際立たせてくれるでしょう。しかし、他の全てのことと同様に、大きなパワーを持つツールには大きな責任が伴うことを覚えておく必要があります。will-changeは甘く見ることをせず、賢く利用すべきプロパティの1つです。ここで、will-change仕様のエディタであるタブ・アトキンス・ジュニアの言葉を引用します。

will-changeは、実際に変化させるプロパティ、実際に変化が生じる要素に設定してください。そして、変化が終了したら削除してください。

読んでくださって、ありがとうございました!

原稿をチェックしフィードバックをくれたポール・ルイス、執筆をサポートし質問に答えてくれたタブ・アトキンス、そして原稿のチェックを手がけてくれたブルース・ローソンとマティアス・バイネンズに心から感謝します。