CSS の will-change の正しい使い方 - 指定しすぎると逆に遅くなる理由

CSS アニメーションのパフォーマンスを改善する手段として will-change がよく紹介されます。ブラウザに「この要素はこれから変化しますよ」と事前に伝えることで、描画の最適化を促すプロパティです。しかし、とりあえず付けておけば速くなるという理解で乱用すると、かえってメモリ消費が増え、パフォーマンスが悪化することがあります。

ブラウザの描画プロセスと合成レイヤー

will-change を理解するには、ブラウザがどのように画面を描画しているかを知る必要があります。ブラウザは HTML を解析してから画面に表示するまで、いくつかの段階を踏みます。

Style(スタイル計算)

Layout(配置計算)

Paint(描画)

Composite(合成)

最後の Composite 段階では、ブラウザは画面をいくつかのレイヤーに分けて個別に描画し、最終的にそれらを重ね合わせて 1 枚の画面を完成させます。transform や opacity のアニメーションが高速なのは、Layout や Paint をスキップして Composite だけで処理できるためです。

will-change は、この合成レイヤーの生成をアニメーション開始前にあらかじめ行わせる仕組みです。通常であればアニメーション開始時に初めてレイヤーが作られますが、will-change を指定しておくと事前にレイヤーが確保されるため、アニメーション開始時のカクつきを防げます。

will-change の基本構文

will-change には、これから変化するであろう CSS プロパティ名を指定します。

.animated-element {
  will-change: transform;
}

複数のプロパティが変化する場合は、カンマ区切りで列挙します。

.animated-element {
  will-change: transform, opacity;
}

この指定により、ブラウザは対象要素を独立した合成レイヤーに昇格させ、GPU 上にテクスチャとして保持する準備を行います。

will-change が有効なケース

will-change が実際に効果を発揮するのは、アニメーション開始時に一瞬カクつく問題を解消したい場面です。

ホバーで変化する要素

ユーザーがホバーしたときに transform や opacity がアニメーションする要素。ホバーの瞬間にレイヤー生成が走ると 1 フレーム目がカクつくことがある。

スクロール連動アニメーション

スクロールに応じて position: fixed の要素や parallax 効果のある要素が動く場面。事前にレイヤーを確保しておくとスムーズに動き出す。

JavaScript で動的に変化させる要素

JavaScript から頻繁に transform を更新する要素。アニメーション開始直前に will-change を付け、終了後に外すのが理想的。

なぜ乱用すると遅くなるのか

will-change を指定すると、ブラウザはその要素を独立した合成レイヤーに昇格させます。レイヤーの実体は GPU メモリ上に保持されるテクスチャ(ビットマップ画像)です。つまり、will-change を指定した要素の数だけ GPU メモリが消費されます。

少数の要素に will-change

レイヤー数が限られており、GPU メモリの消費も少ない。合成処理も軽く、アニメーションがスムーズに動く。

大量の要素に will-change

レイヤーが大量に作られ、GPU メモリを圧迫する。レイヤーの管理・合成コストも増大し、かえって描画が遅くなる。

特に問題になるのが、すべての要素に一括で指定してしまうパターンです。

/* 絶対にやってはいけない */
* {
  will-change: transform;
}

これはページ上のすべての要素に対して合成レイヤーを作成させることになり、メモリ消費が爆発的に増加します。モバイルデバイスのように GPU メモリが限られている環境では、ブラウザがメモリ不足に陥り、ページ全体の描画パフォーマンスが著しく低下する原因になります。

will-change が引き起こす副作用

パフォーマンスの問題以外にも、will-change にはいくつかの副作用があります。意図せず発動してレイアウトが崩れることがあるため、把握しておく必要があります。

新しいスタッキングコンテキストの生成

will-change に transform や opacity を指定すると、その要素は新しいスタッキングコンテキストを作ります。z-index の重なり順が変わり、position: fixed の子要素が固定されなくなることがあります。

新しい包含ブロックの生成

will-change: transform を指定した要素は、position: fixed の子要素にとっての包含ブロックになります。その結果、fixed 要素がビューポート基準ではなく親要素基準で配置されてしまいます。

サブピクセルレンダリングの変化

レイヤーに昇格した要素は、テキストのサブピクセルレンダリング(アンチエイリアシング)が変わることがあります。フォントの見た目が微妙に太くなったりぼやけたりする現象が起きます。

.modal-overlay {
  will-change: opacity;
  /* この要素の中の position: fixed が
     ビューポート基準で効かなくなる可能性がある */
}

正しい使い方 - 必要なときだけ付けて外す

will-change のベストプラクティスは、アニメーションの直前に付けてアニメーション終了後に外すことです。CSS だけで完結させる場合は、親要素のホバー時に子要素へ will-change を付けるパターンが有効です。

.card:hover .card-image {
  will-change: transform;
}

.card-image {
  transition: transform 0.3s ease;
}

.card:hover .card-image {
  transform: scale(1.05);
}

この書き方では、ユーザーがカードにホバーした瞬間に will-change が適用されます。ホバーしてから実際にマウスが .card-image に到達するまでの数十ミリ秒の間にブラウザがレイヤーを準備できるため、アニメーション開始時のカクつきを回避できます。ホバーが外れれば will-change も自動的に解除されます。

JavaScript で制御する場合は、アニメーション開始前にセットし、完了後に解除するのが定石です。

.animating {
  will-change: transform, opacity;
}
const el = document.querySelector('.target');

// アニメーション開始前にセット
el.classList.add('animating');

el.addEventListener('transitionend', () => {
  // アニメーション完了後に解除
  el.classList.remove('animating');
}, { once: true });

will-change を常時指定してもいいケース

原則としてアニメーション前後で付け外しするのが望ましいですが、例外的に常時指定が許容されるケースもあります。

ページ内で頻繁にアニメーションする要素(常に表示されているローディングスピナーなど)
ユーザー操作に即座に反応する必要がある要素(ドラッグ可能な要素など)
付け外しのタイミングを CSS だけで制御できない場合の妥協策

ただし、常時指定する要素の数は最小限に抑えることが前提です。ページ全体で 2〜3 個程度に留めるのが目安になります。

DevTools でレイヤーを確認する

will-change が実際にレイヤーを生成しているかどうかは、Chrome DevTools の Layers パネルで確認できます。

Chrome DevTools を開き、右上のメニューから More tools → Layers を選択すると、ページ上の合成レイヤーが 3D 表示で可視化されます。

各レイヤーのメモリ消費量や生成理由も確認でき、不要なレイヤーの発見に役立つ。

will-change を指定した要素が独立したレイヤーとして表示されていれば、ブラウザが意図どおりに最適化を行っている証拠です。逆に、想定外の要素がレイヤー化されている場合は、will-change の指定範囲を見直す必要があります。

will-change は「おまじない」のように使うものではなく、ブラウザの描画の仕組みを理解したうえで、狙った要素にだけ適用するプロパティです。必要なときだけ付けて不要になったら外す。この原則を守れば、アニメーションの初動を確実に改善できます。