will-changeの使いどころと注意点!CSSアニメーション最適化術

css
記事内に広告が含まれています。

CSSアニメーションを最適化する手段として「will-change」という指定を見かけたことはないでしょうか。確かに効果的な手法として知られていますが、実務では「効果がありそうだから」という理由で多くの要素に付けてしまい、かえってパフォーマンスを悪化させてしまうエンジニアが少なくありません。

will-changeは「ブラウザへの事前通知」に過ぎず、すべてのプロパティに対して効果があるわけではなく、また常時指定することは逆効果です。transformやopacityといった特定のプロパティに限定的に使い、アニメーション開始前に付与して終了後に削除するという「オンデマンド運用」こそが、真の活用法なのです。

本記事では、ブラウザのレンダリングパイプラインという根拠に基づいて、どのプロパティに使うべきか、いつどのタイミングで付与・削除するべきか、そしてTailwind CSSやReactといったモダン開発環境ではどう実装するべきかを、実務レベルの具体例と失敗パターンを交えて詳しく解説します。

この記事を読んでわかること

  • will-changeがブラウザのレンダリングパイプラインでどう機能するのか、という仕組みの本質
  • transformやopacityなど「推奨プロパティ」と、height・widthなど「効果のないプロパティ」を見極める判断基準
  • アニメーション開始直前に付与し、終了直後に削除するという正しいタイミング管理の方法
  • メモリリークや過剰なGPU負荷を避けるための、will-changeの付与・削除ベストプラクティス
  • Tailwind CSSのカスタムクラスやReactのカスタムフックを使った、モダン開発環境での安全な実装パターン
  • will-changeの過剰使用がパフォーマンスを悪化させる理由と、モバイル環境での特別な注意点
  • よくある誤解(効果は必ずあるのか、他のハック的手法との違いは何か、など)への実践的な答え

CSS will-changeとは?意味・仕組み・パフォーマンスへの効果

will-changeは、ブラウザに対して「これから変化するプロパティ」を事前に伝えるためのCSSプロパティです。正しく使えばアニメーションが滑らかになりますが、仕組みを理解せずに使うと逆にパフォーマンスを悪化させます。まずは基本の構文と挙動を押さえましょう。

will change イメージ

will-changeの名前の意味と役割

will-changeは直訳すると「これから変化する」という意味で、その名の通りブラウザへの事前通知として機能します。

ブラウザは通常、要素のスタイルが実際に変化してから、レンダリングパイプライン(スタイル計算→レイアウト→ペイント→コンポジット)の最適化を検討します。will-changeを指定すると、ブラウザは該当プロパティの変化を見越して、あらかじめその要素を独立したコンポジットレイヤーに昇格させておくことができます。

コンポジットレイヤーとは、GPU上で別々に描画・合成される描画単位のことです。transformやopacityの変化をこのレイヤー内で完結させられれば、CPU側のレイアウト再計算やピクセルの再描画を伴わずに、GPUだけでアニメーションを処理できます。これがwill-changeによる高速化の本質です。

ただし、この事前昇格にはメモリコストが伴います。よくある失敗談として、「効果がありそうだから」という理由で多数の要素にwill-changeを付けてしまい、コンポジットレイヤーが乱立してGPUメモリを圧迫し、かえってページ全体がカクつくケースがあります。will-changeは「魔法の高速化タグ」ではなく、あくまで限定的に使うヒントだと理解しておく必要があります。

/* 悪い例:とりあえず全部に付けておく(アンチパターン) */
* {
  will-change: transform;
}

/* 良い例:アニメーションする要素だけに限定する */
.card--animatable {
  will-change: transform;
}
ブラウザレンダリングパイプライン

基本構文:指定できる値(プロパティ名・キーワード値)と書き方

will-change には、変化させたい CSSプロパティ名 のほかに、ブラウザに特定の動作の変化を伝える 特別なキーワード値 を指定できます。

/* 1. デフォルト:特別な最適化を行わない */
.box {
  will-change: auto;
}

/* 2. プロパティ名の指定:transform の変化に備えてコンポジットレイヤーを事前生成 */
.box--will-transform {
  will-change: transform;
}

/* 3. プロパティ名の指定:opacity の変化に備える場合 */
.box--will-opacity {
  will-change: opacity;
}

/* 4. キーワード値:要素のスクロール位置の変化を事前通知 */
.scroll-container {
  will-change: scroll-position;
}

/* 5. キーワード値:子要素やコンテンツ自体の変化を事前通知 */
.dynamic-content {
  will-change: contents;
}

指定できる値の種類と用途

  • auto(デフォルト):ブラウザの通常の最適化に任せる指定です。アニメーション終了後のクリーンアップ時や、最適化を解除したい時にこの値に戻します。
  • CSSプロパティ名(<custom-ident>):transform、opacity、top、left など、これから変化するプロパティ名を直接記述します。
  • scroll-position(キーワード値):その要素が近くスクロールされる(またはスクロール位置が変化する)ことをブラウザに伝えます。指定すると、ブラウザはスクロール枠の範囲外にあるコンテンツをあらかじめレンダリング(事前描画)し、高速で滑らかなスクロールを実現しようと試みます。(※Tailwind CSSの will-change-scroll クラスはこの指定に対応しています)
  • contents(キーワード値):その要素の子要素(コンテンツ)が頻繁に書き換わる、または変化することを伝えます。ブラウザはその要素自体ではなく、子要素の再描画に備えた内部キャッシュの最適化を行います。

注意点:will-change に存在しないプロパティ名や誤字(例: will-change: transforms や will-change: opacities)を指定しても構文エラーにはならず、単に無視されます。正しく機能しているか確認するため、実装後はブラウザのDevTools(Computedタブ)でプロパティが意図通り評価されているかを必ず確認しましょう。

複数プロパティ指定:will-change: transform, opacity; の正しい記法

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

/* 良い例:カンマ区切りで複数指定 */
.modal {
  will-change: transform, opacity;
}

/* 悪い例:スペース区切りは無効(構文エラーとして無視される) */
.modal--invalid {
  will-change: transform opacity;
}

hover時にscaleとopacityを同時に動かすモーダルやカードUIなど、複数プロパティを同時アニメーションさせる場面では、この複数指定が有効です。ただし、指定するプロパティ数が増えるほど、ブラウザが確保するメモリやレイヤー構造は複雑になります。「動かす予定がないプロパティまで念のため含める」といった書き方は避け、実際にアニメーションさせるプロパティだけに絞り込むのが実務上の鉄則です。

なお、複数指定時の内部的なメモリ確保量やレイヤー分割の挙動はブラウザエンジン(Blink・WebKit・Geckoなど)やバージョンによって差があり、すべての環境で同一の最適化がなされるとは限らない点には留意してください。

◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆

will-changeの使いどころ:使うべきケース/使ってはいけないケース

will-changeの効果は「どのプロパティが対象か」によって大きく変わります。実務では、レンダリングパイプラインのどの段階で処理が走るかを判断基準に、適用するかどうかを決めることが重要です。推奨・要検証・非推奨の3つのカテゴリに分けて説明します。

will change イメージ

推奨事例:transformやopacityを用いたhover・scale・translateアニメーション

transformとopacityは、ブラウザのレンダリングパイプラインにおいて「Composite(合成)」段階だけで完結します。つまり、レイアウト計算やペイント処理を伴わず、GPUがコンポジットレイヤーを再合成するだけで済みます。このため、will-changeの恩恵が最も大きいプロパティです。

具体的には、ホバー時のスケール変化、カード要素のスライドイン、モーダルのフェードインなど、UIの基本的なアニメーションの多くがこれに当たります。will-changeを指定すれば、ブラウザはあらかじめこの要素を独立したコンポジットレイヤーに昇格させ、アニメーション開始時のレイアウト計算コストを削減できます。

/* 良い例:hoverアニメーションにtransformを使用 */
.card {
  transition: transform 0.3s ease;
}

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

/* will-changeを指定するclass */
.card.is-animating {
  will-change: transform;
}

JavaScriptでマウスホバーやスクロール時にこのクラスを付与する例:

// 良い例:アニメーション開始前にwill-changeを付与
const card = document.querySelector('.card');

card.addEventListener('mouseenter', () => {
  card.classList.add('is-animating');
});

card.addEventListener('mouseleave', () => {
  card.classList.remove('is-animating');
});

opacityについても同じロジックが当てはまります。フェードイン・フェードアウト効果は、opacityの変化だけでコンポジット段階で完結するため、will-changeの適用効果が顕著です。

60fps(フレーム/秒)でのアニメーションが必要とされるスマートフォンなどのモバイルデバイスでは、この最適化の有無が明らかなカクつきの差を生じます。特に複数の要素が同時にアニメーションする場面(リスト項目の一括スライドインなど)では、will-changeの有無で30fpsと60fpsの差が出るケースもあります。

要検証事例:filterやbackdrop-filterなど描画負荷が高いプロパティへの適用

filterやbackdrop-filterは、ガウスぼかしやセピア調などの画像フィルター効果を実現するプロパティです。これらはペイント段階で大きな計算コストを伴い、アニメーション対象にするとブラウザに負荷がかかります。

will-changeを指定することで、ブラウザはこのプロパティの変化に備えてリソース確保を試みます。ただし、効果のほどはブラウザエンジンやハードウェアスペック(GPU性能)に大きく依存するため、「必ず高速化される」とは保証できません。モバイル端末やメモリが少ない環境では、逆にメモリ圧迫によるパフォーマンス悪化を招く可能性があります。

/* 要検証:filterをアニメーションさせる場合 */
.overlay {
  filter: blur(0px);
  transition: filter 0.5s ease;
}

.overlay.is-active {
  filter: blur(10px);
}

.overlay.needs-optimization {
  will-change: filter;
}

「要検証」と呼ぶ理由は、実装後にデバイスやブラウザ環境ごとにパフォーマンス計測を行い、改善効果を検証する必要があるからです。DevToolsのPerformanceパネルでフレームレートを確認し、will-changeを付けた場合と外した場合で実測値を比較することをお勧めします。

backdrop-filterについても同様で、ブラー背景などの効果は計算が重く、環境依存性が高いため、本番環境での計測が不可欠です。

非推奨事例:height・width・background-positionなどレイアウト再計算が走る要素

heightやwidthなどのボックスモデルプロパティ、あるいはmarginやpaddingは、変化するたびにレイアウト計算(リフロー)が走ります。リフローが発生すると、その要素だけでなく親要素や兄弟要素にも影響が波及し、ページ全体の再計算に至る場合があります。

will-changeを指定しても、根本的なリフロー処理は避けられません。ブラウザがコンポジットレイヤーを事前生成したとしても、レイアウト段階での計算コストは変わらないため、will-changeの効果は期待できず、メモリだけが無駄に消費されます。

これが「非推奨」と呼ぶ理由です。同様に、background-positionやbackground-sizeの変化も、背景画像の再描画が必要になるため、will-changeの対象として相応しくありません。

/* 悪い例:height/widthの変化にwill-changeを指定(効果なし) */
.accordion-item {
  height: 0;
  overflow: hidden;
  will-change: height; /* 無効。むしろメモリ浪費 */
}

.accordion-item.is-open {
  height: auto;
}

/* CSSによる簡易実装例(注意:max-heightもレイアウト再計算が発生するため will-change は無効) */
.accordion-item {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
  /* will-change: max-height; 指定してもリフローは回避できないため不要 */
}

.accordion-item.is-open {
  max-height: 500px; /* 実際の開いた状態の高さに設定 */
}

注意: CSSのみで手軽にアコーディオンを作る際によく使われる max-height のトランジションですが、max-height も height と同じく毎フレームでレイアウト再計算(Reflow)を引き起こします。 そのため、will-change: max-height を指定しても処理は高速化されず、無駄なメモリを消費するだけになります。

パフォーマンスを最優先し、will-change の恩恵を最大限に活かしたい場合は、以下のように transform: scaleY() で代替する手法が有効です。scaleY はコンポジット段階だけで完結するため、レイアウト再計算を完全に回避でき、will-change: transform による最適化が正しく機能します。
(※ただし、scaleY は要素全体が拡大縮小する見た目になるため、内部のテキストや画像の変形に副作用が出ないか確認して採用してください)

/* 推奨案(パフォーマンス優先):transformでスケーリング(コンポジット段階で完結) */
.accordion-item {
  transform-origin: top;
  transform: scaleY(0);
  transition: transform 0.3s ease;
}

.accordion-item.will-animate {
  will-change: transform; /* コンポジット処理で完結するため極めて有効 */
}

.accordion-item.is-open {
  transform: scaleY(1);
}

失敗例として、CSSフレームワークのトランジションやアニメーションライブラリがheightを内部的に使用しており、その上からwill-changeを付け足したものの効果が出ず、「will-changeは使えないプロパティだ」と誤解するケースがあります。根本的には対象プロパティの特性を見極めることが最優先です。

will-changeは常時指定しない|適切な付与・解除タイミング

will-changeは指定するだけでメモリリソースを消費します。「効果があるから常時付けておこう」という考えは、むしろパフォーマンスを悪化させます。重要なのは、アニメーション開始直前に付与し、終了直後に解除するという「オンデマンド運用」です。

will change イメージ

アニメーション開始前にwill-changeを指定するclass付与パターン

CSSクラスを活用して、アニメーション実行時のみwill-changeを有効にする方法が基本となります。マークアップ側では通常のHTML構造を保ち、JavaScriptやCSSのhover疑似クラスを使ってクラスを付与するアプローチです。

<!-- HTMLマークアップ:基本的な構造 -->
<div class="card">
  <h3>コンテンツ</h3>
</div>
/* CSS:基本スタイル */
.card {
  transition: transform 0.3s ease, opacity 0.3s ease;
  transform: scale(1);
  opacity: 1;
}

/* アニメーション用の準備状態 */
.card.will-animate {
  will-change: transform, opacity;
}

/* ホバー時の状態変化 */
.card:hover {
  transform: scale(1.05);
  opacity: 0.9;
}

ホバーの場合はCSS疑似クラスだけでも十分ですが、JavaScriptで動的にアニメーションをトリガーする場合はクラス付与が必須です。以下は、クリック時にアニメーションを実行するパターンです。

// 良い例:アニメーション開始時にクラスを付与
const button = document.querySelector('.button');
const box = document.querySelector('.box');

button.addEventListener('click', () => {
  // アニメーション開始前にwill-changeクラスを付与
  box.classList.add('will-animate');

  // アニメーションをトリガー
  box.classList.add('is-active');
});

この方法の利点は、マークアップにwill-changeを直接書き込まず、必要な時だけCSSクラスで注入できる点です。将来的にアニメーション戦略を変更する場合も、CSS側の調整だけで対応でき、HTMLを再コンパイルする必要がありません。

アニメーション終了後にwill-change: autoへ戻す方法

will-changeの効果を一度付与すると、明示的に解除するまでメモリが消費され続けます。実務での典型的な失敗が、アニメーション開始時だけクラスを付与し、終了後に外し忘れるケースです。これにより、ページ内に不要なコンポジットレイヤーが蓄積し、スクロール時や他のアニメーション実行時にメモリ圧迫が生じます。

解除タイミングは、アニメーション終了のわずか後です。JavaScriptのtransitionendイベントやanimationendイベントをリスンして、該当のクラスを外すのが確実です。

// 悪い例:アニメーション終了後の削除忘れ
const box = document.querySelector('.box');

box.addEventListener('click', () => {
  box.classList.add('will-animate');
  box.classList.add('is-active');
  // ここでwill-animateを外さない → メモリ漏洩
});
// 良い例:animationend/transitionendイベントで確実に削除
const box = document.querySelector('.box');

box.addEventListener('click', () => {
  box.classList.add('will-animate');
  box.classList.add('is-active');
});

// トランジション終了時にwill-changeクラスを削除
box.addEventListener('transitionend', () => {
  box.classList.remove('will-animate');
});

より詳細な制御が必要な場合は、アニメーション期間の管理をJavaScriptで行う方法もあります。例えば、transform: scaleを0.3秒でアニメーションさせるなら、クラス付与から0.3秒後に外すといったアプローチです。

// 別案:setTimeout で明示的に解除タイミングを設定
const box = document.querySelector('.box');
const ANIMATION_DURATION = 300; // ミリ秒

box.addEventListener('click', () => {
  box.classList.add('will-animate');
  box.classList.add('is-active');

  // アニメーション期間後にwill-changeを削除
  setTimeout(() => {
    box.classList.remove('will-animate');
  }, ANIMATION_DURATION);
});

transitionendイベントの方がより堅牢です。理由は、CSSの書き換えやメディアクエリの変更によってアニメーション期間が変わった場合も、自動的に対応できるからです。JavaScriptとCSSの二重管理を避けられます。

JavaScriptでwill-changeを動的に追加・削除するベストプラクティス

複数の要素にアニメーションを適用する場合や、複雑な条件下でwill-changeを管理する場合は、専用の関数を作成してベストプラクティスを統一するのが有効です。

以下は、要素のアニメーション状態を管理するユーティリティ関数の例です。

// ベストプラクティス:will-changeの付与・削除を管理するヘルパー関数
class AnimationManager {
  static async animateWithWillChange(element, className, options = {}) {
    const { duration = 300, willChangeClass = 'will-animate' } = options;

    try {
      // アニメーション開始前にwill-changeを付与
      element.classList.add(willChangeClass);

      // 次フレームで状態変化をトリガー(ブラウザの最適化を促進)
      await new Promise(resolve => requestAnimationFrame(() => resolve()));

      // アニメーションクラスを付与
      element.classList.add(className);

      // アニメーション終了を待つ
      await this.waitForTransitionEnd(element, duration);

      // アニメーション終了後にwill-changeを削除
      element.classList.remove(willChangeClass);
    } catch (error) {
      console.error('Animation failed:', error);
      // エラー発生時もクリーンアップ
      element.classList.remove(willChangeClass);
    }
  }

  static waitForTransitionEnd(element, fallbackDuration) {
    return new Promise((resolve) => {
      const onTransitionEnd = () => {
        element.removeEventListener('transitionend', onTransitionEnd);
        resolve();
      };

      // イベントリスナを設定
      element.addEventListener('transitionend', onTransitionEnd, { once: true });

      // イベントが発火しない場合のフォールバック
      setTimeout(() => {
        element.removeEventListener('transitionend', onTransitionEnd);
        resolve();
      }, fallbackDuration + 50);
    });
  }
}

// 使用例
const card = document.querySelector('.card');
AnimationManager.animateWithWillChange(card, 'is-active', {
  duration: 300,
  willChangeClass: 'will-animate'
});

このアプローチの利点:

  1. 統一性:すべてのアニメーションが同じロジックで管理される
  2. エラーハンドリング:例外発生時もwill-changeが確実に削除されるため、メモリリーク防止
  3. フォールバック:transitionendイベントが発火しない環境(稀)にも対応
  4. 保守性:アニメーション管理の仕様変更時に、この関数を修正するだけで全体に反映

注意点として、複数の要素が同時にアニメーションする場合、バッチ的にwill-changeを付与するのは避けてください。例えば、リスト内の10個の要素を同時にスライドインさせる場合、すべて同時にwill-changeを付与すると、コンポジットレイヤーが一気に10個生成され、GPU負荷が急増します。

// 悪い例:複数要素に同時にwill-changeを付与
document.querySelectorAll('.item').forEach(item => {
  item.classList.add('will-animate');
  item.classList.add('is-active');
});

// 良い例:スタッガー効果でwill-changeのタイミングをずらす
document.querySelectorAll('.item').forEach((item, index) => {
  setTimeout(() => {
    item.classList.add('will-animate');
    item.classList.add('is-active');

    item.addEventListener('transitionend', () => {
      item.classList.remove('will-animate');
    }, { once: true });
  }, index * 50); // 50msずつズラす
});

スタッガー効果により、GPU負荷を時間軸で分散させることができます。見た目上のカスコーディング(段階的な発動)という利点もあり、モダンなUIデザインでは積極的に採用される手法です。

WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクール

Tailwind CSSやReactなどモダン開発環境での活用テクニック

Tailwind CSSやReactといったモダンなフレームワーク・ツールを使う場合、will-changeの運用方法は従来のCSSとは異なります。各ツールの特性に合わせた、実務的な実装パターンを紹介します。

will change react イメージ

Tailwind CSSのユーティリティクラス(will-change-transform等)の使い方と独自拡張

Tailwind CSSは標準でwill-change-autowill-change-transformwill-change-opacitywill-change-scrollといったユーティリティクラスを提供しています。これらを活用することで、CSSファイルを別途作成せずにwill-changeを管理できます。

<!-- Tailwind標準ユーティリティの使用例 -->
<div class="card hover:scale-105 transition-transform duration-300 will-change-transform">
  <h3>カード内容</h3>
</div>

しかし実務では、「アニメーション中だけwill-changeを付与したい」というニーズが生じます。Tailwindの標準ユーティリティは常時適用されるため、状態に応じた動的な付与・削除を実現するには、tailwind.config.jsで独自に拡張する必要があります。

// tailwind.config.js での拡張例
module.exports = {
  theme: {
    extend: {
      willChange: {
        // 標準は auto, transform, opacity, scroll のみ
        // 必要に応じて追加
        filter: 'filter',
        'backdrop-filter': 'backdrop-filter',
      },
    },
  },
  plugins: [],
};

ただし、Tailwindの宿命として、動的クラス名の生成はビルド時に確定されるため、JavaScriptでwill-change-transformクラスを完全に動的に付与・削除する場合は、セーフリスト(safelist)に明示的に記述する必要があります。

// tailwind.config.js:セーフリストで動的クラスを許可
module.exports = {
  safelist: [
    'will-change-transform',
    'will-change-opacity',
    'will-change-filter',
  ],
  theme: { /* ... */ },
};

セーフリストを使うと、これらのクラスは常に最終的なCSSビルドに含まれます。一見すると手軽ですが、実装としては「不要になったクラスもビルド時に残る」という問題があり、CSSバンドルサイズが増加します。

より実務的なアプローチは、変動しない構造部分(ホバー状態など)には標準ユーティリティを使い、JavaScriptで動的に付与する部分は従来の@layer utilitiesを使ったカスタムクラスで補うことです。

/* globals.css:カスタムユーティリティの定義 */
@layer utilities {
  .will-animate-transform {
    @apply will-change-transform;
  }

  .will-animate-opacity {
    @apply will-change-opacity;
  }

  /* 複数プロパティ指定が必要な場合 */
  .will-animate-multi {
    will-change: transform, opacity;
  }
}
<!-- HTML/JSXテンプレート -->
<div id="card" class="transition-transform duration-300 hover:scale-105">
  <h3>アニメーション対象</h3>
</div>
// JavaScript:カスタムクラスを動的に付与
const card = document.getElementById('card');

card.addEventListener('mouseenter', () => {
  card.classList.add('will-animate-transform');
});

card.addEventListener('mouseleave', () => {
  // アニメーション終了後に削除
  card.addEventListener('transitionend', () => {
    card.classList.remove('will-animate-transform');
  }, { once: true });
});

この方法であれば、Tailwindの最適化恩恵を活かしつつ、動的管理も実現できます。

Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.
Tailwind CSS is a utility-first CSS framework for rapidly building modern websites without ever leaving your HTML.

React/Next.jsにおけるコンポーネントライフサイクルに応じた安全な着脱処理

Reactコンポーネントでwill-changeを管理する場合、useEffectフックを活用してライフサイクルに同期させるのが標準的です。重要なのは、クリーンアップ処理でwill-changeを確実に削除し、メモリリークを防ぐことです。

// 基本的な例:useStateとuseEffectを使用
import { useState, useEffect, useRef } from 'react';

function AnimatedCard() {
  const [isAnimating, setIsAnimating] = useState(false);
  const cardRef = useRef(null);

  const handleAnimationStart = () => {
    setIsAnimating(true);
  };

  useEffect(() => {
    const card = cardRef.current;
    if (!card) return;

    if (isAnimating) {
      // アニメーション開始前にwill-changeを付与
      card.classList.add('will-change-transform');

      // アニメーションをトリガー
      card.classList.add('is-active');

      // アニメーション終了時にwill-changeを削除
      const handleTransitionEnd = () => {
        card.classList.remove('will-change-transform');
        setIsAnimating(false);
      };

      card.addEventListener('transitionend', handleTransitionEnd, { once: true });

      // クリーンアップ
      return () => {
        card.removeEventListener('transitionend', handleTransitionEnd);
      };
    }
  }, [isAnimating]);

  return (
    <div
      ref={cardRef}
      className="card transition-transform duration-300"
      onClick={handleAnimationStart}
    >
      クリックでアニメーション
    </div>
  );
}

次に、より実用的なカスタムフックを作成するアプローチです。このパターンなら、複数コンポーネントで再利用できます。

// 修正版カスタムフック:useWillChange
import { useState, useEffect } from 'react';

function useWillChange(ref, properties = ['transform'], duration = 300) {
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    const element = ref.current;
    if (!element || !isActive) return;

    // DOMのstyleプロパティに直接 will-change を設定
    // (TailwindのJITビルド制限を回避し、複数プロパティの指定にも安全に対応)
    const propertyValue = properties.join(', ');
    element.style.willChange = propertyValue;

    // トランジション終了時にクリーンアップ(autoへ戻す)
    const handleEnd = () => {
      element.style.willChange = 'auto';
      setIsActive(false);
    };

    // フォールバック付きのタイムアウト設定
    const timerId = setTimeout(handleEnd, duration + 50);
    element.addEventListener('transitionend', handleEnd, { once: true });

    return () => {
      clearTimeout(timerId);
      element.removeEventListener('transitionend', handleEnd);
      element.style.willChange = 'auto'; // アンマウント時も確実に解除
    };
  }, [isActive, duration, properties]);

  return { isActive, setIsActive };
}

// 使用例
function AnimatedButton() {
  const ref = useRef(null);
  // 複数プロパティを指定しても 'transform, opacity' として正しく適用される
  const { isActive, setIsActive } = useWillChange(ref, ['transform', 'opacity'], 300);

  return (
    <button
      ref={ref}
      className={`px-4 py-2 transition-all ${isActive ? 'scale-95 opacity-75' : ''}`}
      onClick={() => setIsActive(true)}
    >
      クリック
    </button>
  );
}

Next.jsにおいては、サーバーサイドレンダリング(SSR)の影響を考慮する必要があります。will-changeはDOM操作を伴うため、useEffect内で実行し、サーバーサイドでは実行されないようにしましょう。

// Next.js での安全な実装
'use client'; // クライアントコンポーネントとして明示

import { useEffect, useRef } from 'react';

function SafeAnimatedComponent() {
  const ref = useRef(null);

  useEffect(() => {
    // このコード内はクライアントサイドのみで実行される
    const element = ref.current;
    if (!element) return;

    element.classList.add('will-change-transform');

    return () => {
      element.classList.remove('will-change-transform');
    };
  }, []); // マウント時のみ実行

  return <div ref={ref}>コンテンツ</div>;
}

実務での落とし穴として、Reactの開発時にStrictModeが有効になっていると、useEffectが二重実行されることがあります。これにより、クラスの付与・削除が重複実行され、予期しない動作が生じる可能性があります。本番ビルドでは問題ありませんが、開発時に挙動が異なる点は認識しておくべきです。

CSS Scroll-driven AnimationsやView Transitions APIとの併用要否

CSS Scroll-driven Animations(スクロール駆動アニメーション)やView Transitions API(ビュー遷移)は、比較的新しいブラウザAPIで、will-changeと併用する場合の相互作用について、環境依存性が高い状況です。

CSS Scroll-driven Animationsは、animation-timeline: view()などを使ってスクロール位置に連動させるアニメーションです。この場合、アニメーション対象のプロパティに対してwill-changeを指定しても、パフォーマンス向上効果が明確ではありません。理由は、スクロール駆動アニメーションが内部的にすでに最適化されているためです。

/* スクロール駆動アニメーション */
.fade-in {
  animation: fadeIn linear;
  animation-timeline: view();
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* will-changeの併用について:検証が必要 */
.fade-in.will-optimize {
  will-change: opacity;
}

ブラウザ間での実装差が大きく、Chromeではwill-changeの効果が測定できるが、Firefoxでは効果がないといったケースが報告されています。新しい技術であるため、実装後の計測が不可欠です。

View Transitions APIは、SPA(Single Page Application)のページ遷移を滑らかにするAPIで、Next.jsなどのフレームワークでも採用が進んでいます。このAPIを使う場合、will-changeはほぼ不要です。理由は、View Transitions API自体が内部的にアニメーション対象要素を隔離し、GPUレイヤーを最適に管理しているからです。

// Next.js での View Transitions API の使用例
import { useTransitionRouter } from 'next/transitions';

export default function Link({ href, children }) {
  const router = useTransitionRouter();

  const handleClick = async (e) => {
    e.preventDefault();
    // View Transitions APIが自動的に最適化を行う
    // ここでwill-changeを付与する必要はない
    await router.push(href);
  };

  return <a onClick={handleClick}>{children}</a>;
}

ただし、View Transitions API内で独立した要素をさらに細かくアニメーションさせたい場合は、その要素に対してwill-changeを付与する価値があります。

// View Transitions 内で独自のアニメーションを追加する場合
function CardWithCustomAnimation() {
  const [isHovered, setIsHovered] = useState(false);
  const cardRef = useRef(null);

  useEffect(() => {
    if (!cardRef.current) return;

    if (isHovered) {
      cardRef.current.classList.add('will-change-transform');
      cardRef.current.classList.add('is-hovered');

      const cleanup = () => {
        cardRef.current?.classList.remove('will-change-transform');
      };

      cardRef.current.addEventListener('transitionend', cleanup, { once: true });
      return cleanup;
    }
  }, [isHovered]);

  return (
    <div
      ref={cardRef}
      className="transition-transform duration-300"
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      コンテンツ
    </div>
  );
}

使用の判断基準としては、以下のように整理できます:

  • Scroll-driven Animations:将来的な標準化・最適化を待つ段階。現状はwill-changeの効果が不確定のため、必要に応じて計測してから判断する
  • View Transitions API:基本的にはAPI側で最適化されており、will-changeは不要。ただしAPI内で追加アニメーションを行う場合のみ、その要素に限定して使用する

これらの新しいAPIはブラウザベンダーの開発が継続中であり、将来のバージョンアップで最適化が強化される可能性も高いです。したがって「現在は効果がなくても、将来的には必要になるかもしれない」という柔軟な姿勢で、定期的に再検証することをお勧めします。

よくある質問(FAQ)

will-changeを付けすぎるとパフォーマンスが悪化するというのは本当ですか?

はい、本当です。will-changeを指定した要素ごとに独立したコンポジットレイヤーが生成されます。レイヤー数が増えすぎるとGPUメモリが圧迫され、ブラウザはメモリ管理に労力を割かなければならなくなります。結果として、ページ全体のスクロールやアニメーションがかえってカクつきます。目安として、1ページあたり5~10要素程度に限定するのが実務的です。DevToolsのPerformanceパネルで、フレームレート低下がないか定期的に検証しましょう。

will-changeとtransform: translateZ(0)というハック的な書き方の違いは何ですか?

どちらも強制的にコンポジットレイヤーを生成する手法ですが、意図と互換性が異なります。translateZ(0)は「3D変形をさせている」ことになるため、見た目に予期しない挙動が生じる可能性があります。一方、will-changeはあくまでヒント(hint)であり、意図が明確です。モダンブラウザではwill-changeの方が推奨されており、保守性も高いです。ただしレガシーブラウザ(IE11等)との互換性が必須の場合は、両方を併記することもあります。

will-changeはSEOやCore Web Vitals(特にINP)に影響しますか?

直接的なSEO影響はありません。will-changeはCSSのパフォーマンスチューニング手法であり、検索エンジンのクローラーは見えません。ただし、Core Web Vitals(特にINP:Interaction to Next Paint)の観点では、アニメーションがスムーズ(高fpsを維持)になることで、ユーザー体験が向上し、結果的にページ滞在時間が増え、SEOの間接的な加点につながる可能性があります。つまり、will-changeは「SEOスコア→体験向上→ユーザー満足→SEO向上」という長期的な連鎖を形成します。

アニメーション開始の何ミリ秒前にwill-changeを指定するべきですか?

アニメーション開始の10~50ms前に指定するのが目安です。ブラウザがコンポジットレイヤーを事前生成し、最適化を完了させるために、わずかな時間差が必要です。JavaScriptでアニメーションをトリガーする場合は、requestAnimationFrameを使ってwill-changeを付与し、次フレームでアニメーションクラスを追加するという2ステップ処理が確実です。ただし、hoverやfocus疑似クラスで十分な遅延があれば、明示的なタイミング制御は不要な場合も多いです。

モバイル端末でwill-changeを使う際に特別な注意点はありますか?

モバイル端末はデスクトップ比べてGPUメモリとCPU性能が限定的です。will-changeの過剰使用は、バッテリー消費の急増と熱発生につながります。Androidでは特にメモリ圧迫時のページ再読込が多発する懸念があります。実務での推奨は、モバイル環境では逆にwill-changeの使用をより厳選し、複数要素の同時アニメーションは避け、スタッガー効果でタイミングをずらすことです。DevTools(Chrome DevTools)のDevice Emulationで、実際のモバイル環境での計測を必ず行いましょう。

新世代レンタルサーバー『シンレンタルサーバー』

まとめ

will-changeは、CSSアニメーションのパフォーマンス最適化において確かに有力な手法です。ただ、多くのエンジニアが「効果的なら常時付けておこう」という勘違いをしており、それがかえってページ全体のパフォーマンス悪化につながっています。本記事を通じて見てきた通り、will-changeの真価は「必要な時だけ、必要な要素だけに」という限定的な運用にあります。

重要ポイント

  • レンダリングパイプラインの理解が不可欠:transformやopacityはコンポジット段階で完結するため効果的である一方、height・widthなどのレイアウト系プロパティはwill-changeでも高速化できない
  • 常時指定ではなくオンデマンド運用が基本:アニメーション開始直前に付与し、終了直後に削除する。この付与・削除タイミングを正しく実装することが、効果と安全性を両立させる鍵
  • メモリコストとの天秤:コンポジットレイヤーの生成にはGPUメモリが消費される。モバイル環境では特に、複数要素への無分別な付与がバッテリー消費と熱発生を招く
  • 計測による検証は必須:will-changeの効果はブラウザ、デバイス、ハードウェアに依存する。デバイスエミュレーションやPerformanceパネルでの実測なしに、効果を仮定しない
  • モダン開発環境では専用の仕組みが有効:Tailwind CSSのカスタムクラスやReactのカスタムフックを使うことで、コンポーネント単位での一貫した管理が実現でき、保守性が大幅に向上する

これらのポイントを踏まえ、実務での判断軸を整理すれば、will-changeは確実な武器となります。

実務での行動指針

結局のところ、will-changeは「アニメーション対象がtransformやopacityで、複数回トリガーされず、10~50ms前に付与・削除できる」という限られた条件下でのみ、迷いなく採用し、それ以外の状況では計測で効果を確認してから判断する。これが、パフォーマンス悪化を招かず、実務の要求に応えられる堅実なアプローチです。

will-changeは決して魔法ではなく、ブラウザへの事前通知に過ぎません。その通知が活きるのは、正確な理解と慎重な運用あってこそです。これからのプロジェクトで、より良いユーザー体験を実現するために、ぜひこの記事の知見を活用してください。

WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクール
タイトルとURLをコピーしました