CSSのwill-changeを付けるべき場面と付けすぎたときの影響

​​CSS​Chromium,JavaScript,Playwright

CSS の will-change は、これから変化するプロパティをブラウザーに前もって伝えるヒントです。動き出す少し前に付け、動き終わったら外すのが基本の使い方です。付けすぎると、検証環境の Chromium では要素ごとに合成レイヤーが作られ、描画用のメモリーが付けた数に応じて増えました。また will-change: transform や opacity を付けた要素は、値が初期値のままでも z-index の重なり方が変わります。transform では、position: fixed の要素の基準も変わります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、ヘッドレスの Chromium 153 です。合成レイヤーの数と、レイヤーになった理由は Chrome DevTools Protocol の LayerTree ドメインで調べました。描画用のメモリーは、Chromium の memory-infra のメモリーダンプにある cc/tile_memory(画面を描いた結果を保持するタイルのメモリー)の値です。Firefox と Safari、スマートフォンの実機、GPU を使う通常の Chrome では試していません。アニメーションの滑らかさ(フレームレート)も測っていません。

will-change が伝えるのは「これから変わる」という予告

CSS の仕様では、will-change は要素にどんな変化が起きるかをブラウザーに伝えるレンダリングのヒントです。ブラウザーは、その変化を滑らかに描くための準備を前もって済ませられます。値には transform や opacity のようなプロパティ名を、カンマで区切って並べます。

css
.menu {
  will-change: transform;
}

仕様の導入部では、要素を別のレイヤーに移すような準備は手間がかかり、アニメーションの開始が目に見えて遅れることがある、と説明しています。will-change は、その準備を変化が始まる前に済ませるためのものです。

検証環境の Chromium で、will-change: transform を付けた要素を調べると、要素は独立した合成レイヤーになっていました。レイヤーになった理由は WillChangeTransform です。

一方、will-change を付けなくても、transition で transform を動かしている間は、要素が合成レイヤーになりました。理由は ActiveTransformAnimation で、動き終わるとレイヤーはなくなりました。MDN にも、アニメーションの対象のプロパティは、すでに will-change に含まれているかのように扱われると書かれています。will-change が役に立つのは、その準備を動き出す前に済ませておきたいときです。

付けるとよい場面

動き出す少し前に分かるとき

いちばん基本の使い方は、変化が起きそうだと分かった時点でスクリプトから付け、終わったら外すことです。クリックで開くメニューなら、マウスが乗った時点で付けておけます。

js
const menu = document.getElementById("menu");

menu.addEventListener("mouseenter", () => {
  menu.style.willChange = "transform";
});

menu.addEventListener("transitionend", () => {
  menu.style.willChange = "auto";
});

menu.addEventListener("click", () => {
  menu.classList.toggle("open");
});
css
#menu {
  transition: transform 1s linear;
}

#menu.open {
  transform: translateX(300px);
}

検証環境で、この要素の合成レイヤーを順に調べた結果は次のとおりです。

時点 合成レイヤー(理由)
ページを開いた直後 なし
マウスを乗せた後 あり(WillChangeTransform)
クリックして 0.25 秒後(移動中) あり(ActiveTransformAnimation と WillChangeTransform)
移動が終わった後 なし(will-change は auto に戻った)

マウスを乗せた時点でレイヤーが用意され、移動が終わると transitionend で外れて、レイヤーもなくなりました。

will-change を付ける時機についての仕様の説明では、変化の直前に付けても準備が間に合わず、ほとんど効果がないと書かれています。クリックで変わる要素ならホバーで付ければ、人の反応は比較的遅いので、最適化の準備にたいてい少なくとも 200 ミリ秒を使えるとしています。ホバーで変わる要素なら、親要素へのホバーで付ける CSS の例も載っています。

css
.element {
  transition: opacity 0.2s;
  opacity: 1;
}

.container:hover > .element {
  will-change: opacity;
}

.element:hover {
  opacity: 0.3;
}

MDN の will-change のページにも、mouseenter で付けて、アニメーションの終わりで外すスクリプトの例があります。ただし、2026年11月9日に取得した MDN の原稿(日本語版、英語版とも)では、外す側が addEventListener("animationEnd", ...) と大文字の E で書かれていました。イベント名は大文字と小文字を区別します。検証環境で、0.2 秒の CSS アニメーションを付けた要素に animationEnd と animationend の両方の名前で関数を登録すると、呼ばれたのは animationend だけでした(animationEnd は0回、animationend は1回)。例をそのまま写すと、will-change が外れないままになります。

数の少ない UI 部品と、動き続ける要素

スタイルシートに書く場合についての仕様の説明では、スタイルシートに直接書いてよい場合として、2つを挙げています。1つは、ユーザーの操作にすぐ反応させたい、数の少ない UI 部品(引き出して開くサイドバーなど)です。もう1つは、マウスの動きに合わせて動き続ける要素のように、ほぼ常に変化している要素です。

MDN も、キー操作でページを切り替えるスライドのような場面では、スタイルシートに書いたほうが適切なこともあるとしています。そのうえで、不要になったメモリーをブラウザーが持ち続けることがあるので注意するよう書いています。

性能の問題が実際に起きているとき

MDN は、will-change を、すでに起きている性能の問題に対する最後の手段として使うよう警告しています。問題が起きる前に、予防のために付けるものではありません。ページが十分に速く動いているなら、付ける必要はありません。

付けすぎたときの影響

合成レイヤーとメモリーが増える

検証環境で、720×520px のスライドを同じ位置に30枚重ねたページを作りました。このうち何枚に will-change: transform を付けるかを変えて、Chromium のタイルのメモリー(cc/tile_memory)を調べました。

棒グラフ。will-change: transform を付けたスライドの枚数が 0枚で 2.3 MB、1枚で 4.5 MB、5枚で 13.5 MB、10枚で 24.8 MB、20枚で 47.3 MB、30枚で 67.5 MB。30枚の棒だけピンク
will-change を付けた枚数と、Chromium のタイルのメモリー(30枚のスライドを重ねたページ)

付けていないときは、30枚とも1枚の画面としてまとめて描かれ、タイルのメモリーは 2.3 MB でした。付けた枚数が増えるとレイヤーも増え、30枚すべてに付けると 30 個のレイヤーになって、67.5 MB まで増えました。同じページを2回測り、2回とも同じ値でした。

1枚だけに付けたときは、スライドの大きさのレイヤーが2つできました。1つは will-change を付けたスライドのレイヤー(理由は WillChangeTransform)です。もう1つは、理由が Overlap(重なり)と示されたレイヤーでした。付けたスライドの上に重なる、ほかのスライドの部分も、別のレイヤーになりました。

will-change: opacity を30枚に付けたときも、レイヤーは30個、メモリーは 67.5 MB で、transform と同じでした。

付けすぎについての仕様の説明は、* { will-change: transform, opacity; } のようにすべての要素に付ける書き方をよくない例に挙げています。ブラウザーはもともとできるだけ最適化しているので、付けても助けにならず、強い最適化は多くのリソースを使い、このように付けすぎるとページが遅くなったり、クラッシュしたりすることもあるとしています。また仕様の注記では、will-change: transform の要素を GPU のレイヤーに移すブラウザーでも、指定した要素が多すぎるときは GPU のメモリーを使い切らないよう、移さないことがあると書かれています。ここで測った増え方は、Chromium 153 の一例です。

変化が終わった要素についての仕様の説明によると、ブラウザーはふつう、最適化をできるだけ早く解除して元に戻します。will-change はそれを上書きし、最適化を長く保たせます。前の節のスクリプトのように、動き終わったら外すのはこのためです。

z-index の重なり方が変わる

仕様では、初期値以外の値で重ね合わせコンテキストを作るプロパティを will-change に書くと、その要素も重ね合わせコンテキストを作ると決めています。MDN の重ね合わせコンテキストのページにあるとおり、opacity は1未満で、transform は none 以外で重ね合わせコンテキストを作ります。そのため、will-change: opacity や will-change: transform を付けた時点で、値が初期値のままでも重ね合わせコンテキストができます。

検証環境で、position: relative の親の中に z-index: 10 の子を置き、親の外にある z-index: 1 の要素と重ねました。

html
<div id="parent" style="position: relative; will-change: opacity">
  <div id="child" style="position: absolute; z-index: 10">…</div>
</div>
<div id="other" style="position: absolute; z-index: 1">…</div>

親に will-change がないときは、重なった位置で上に見えたのは子(z-index: 10)でした。親に will-change: opacity か will-change: transform を付けると、上に見えたのは親の外の要素(z-index: 1)に変わりました。重ね合わせコンテキストの中の要素は、外の要素とは別に重ねられるので、子の z-index: 10 は親の外の要素と比べられなくなります。親に will-change: top やカスタムプロパティの will-change: --x を付けたときは、子が上のままでした。

position: fixed の要素が画面に固定されなくなる

仕様では、初期値以外の値で固定配置(position: fixed)や絶対配置(position: absolute)の基準になる箱を作るプロパティを will-change に書くと、その要素も基準になる箱を作ると決めています。CSS Transforms の仕様では、transform が none 以外のとき、要素は絶対配置と固定配置の子孫の基準になる箱を作ります。

検証環境で、position: fixed のヘッダーを入れた main 要素に will-change: transform を付け、ページを 300px スクロールしました。

2つの画面を縦に並べたスクリーンショット。上の「main に will-change なし」では、300px スクロールした後も水色の固定ヘッダーが画面の上に残っている。下の「main に will-change: transform」では、ヘッダーが見えず、本文の行だけが見える
position: fixed のヘッダーを含む main に、will-change を付けない場合と付けた場合の、300px スクロールした後の画面

will-change がないときは、スクロールした後もヘッダーの上端は画面の上端(0px)にありました。will-change: transform を付けると、ヘッダーは main を基準に置かれ、本文と一緒に流れて上端が -300px になり、画面から消えました。別に用意した簡単なページで試すと、will-change: filter でも同じようにヘッダーが流れました。will-change: opacity では、ヘッダーは画面に固定されたままでした。

絶対配置でも同じことが起きました。position を指定していない親(margin: 100px)の中に、position: absolute; top: 0; left: 0 の子を置きました。親に will-change がないときは、子はページの左上(0, 0)に置かれました。親に will-change: transform を付けると、子は親の左上(100, 100)に置かれました。will-change: position でも同じ結果でした。

ページ全体を包む要素にまとめて付けると、上の検証のように、中にある固定ヘッダーが画面に固定されなくなります。MDN も、body のように広い範囲に付けると、かえって性能が悪くなりうるとして、なるべく狭い範囲の、深く入れ子になった要素に限って使うよう書いています。

transform と opacity 以外の名前でもレイヤーになることがある

仕様では、ほとんどのプロパティは will-change に書いても効果がなく、書いても安全だとしています。カスタムプロパティを書いた場合は、効果があってはならないと決められています。

ただし検証環境の Chromium では、position: absolute のスライド30枚に will-change: top を付けると、30枚ともレイヤーになり、メモリーは transform を付けたときと同じ 67.5 MB でした。理由は WillChangeOther でした。仕様の例にも、画面を飛び回る要素に will-change: left, top; を付ける書き方が載っています。transform と opacity 以外の名前でも、付けすぎればメモリーが増えることがあります。

ブラウザーの対応

互換性データ(@mdn/browser-compat-data 8.1.8)では、will-change は Chrome 36、Edge 79、Firefox 36、Safari 9.1、iOS の Safari 9.3 から使えます。

​Posted by 86