CSSのパフォーマンス最適化でよく使われるwill-changetransform: translateZ(0)。これらは確かにアニメーションを滑らかにする効果がありますが、予期しない副作用を引き起こすことがあります。

その代表的な問題が「子要素のposition: fixedが正常に動作しなくなる」現象です。

この記事では、なぜこの問題が発生するのか、その仕組みと解決策を実例とともに解説します。


    1. will-changeとtransform: translateZ(0)の基本

    will-changeとは

    will-changeは、ブラウザに「このプロパティが近いうちに変更される」と事前に伝えるCSSプロパティです。

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

    目的:

    • ブラウザが最適化の準備をする
    • GPUアクセラレーションを有効化
    • アニメーションのパフォーマンス向上

    transform: translateZ(0)とは

    3D transformの一種で、Z軸方向に0px移動する(つまり見た目は変わらない)指定です。

    .element {
      transform: translateZ(0);
    }
    

    目的:

    • 強制的にGPUレイヤーを作成
    • ハードウェアアクセラレーションを有効化
    • いわゆる「ハック」として使われてきた

    類似のプロパティ

    以下のプロパティも同様の効果(と副作用)があります:

    /* GPUアクセラレーションを有効にするプロパティ */
    transform: translate3d(0, 0, 0);
    transform: perspective(1px);
    backface-visibility: hidden;
    

    2. スタッキングコンテキストとは

    基本概念

    スタッキングコンテキストとは、要素の重なり順序を決定する3D空間のような概念です。新しいスタッキングコンテキストが作成されると、その中の要素は独立した「層」として扱われます。

    スタッキングコンテキストを作成する条件

    以下のいずれかを持つ要素は、新しいスタッキングコンテキストを作成します:

    1. positionがrelative/absolute/fixed で z-indexが auto以外

      .element {
        position: relative;
        z-index: 1; /* スタッキングコンテキスト作成 */
      }
      
    2. transformプロパティ(none以外)

      .element {
        transform: translateZ(0); /* スタッキングコンテキスト作成 */
      }
      
    3. will-change で transform, opacity等を指定

      .element {
        will-change: transform; /* スタッキングコンテキスト作成 */
      }
      
    4. その他

      • opacityが1未満
      • filter, backdrop-filter, clip-path
      • isolation: isolate
      • contain: layout, paint, strict, content

    3. position: fixedが壊れる理由

    通常のposition: fixedの動作

    position: fixedは通常、ビューポート(画面全体)を基準に配置されます。

    <div class="parent">
      <div class="fixed-child">
        固定要素(画面に対して固定)
      </div>
    </div>
    
    .fixed-child {
      position: fixed;
      top: 0;
      right: 0;
      /* 通常は画面右上に固定される */
    }
    

    transformやwill-changeがある場合

    親要素にtransformwill-change: transformがある場合、position: fixedの基準点が変わります。

    .parent {
      transform: translateZ(0); /* スタッキングコンテキスト作成 */
    }
    
    .fixed-child {
      position: fixed;
      top: 0;
      right: 0;
      /* ビューポートではなく.parentを基準に配置される! */
    }
    

    なぜこうなるのか

    CSS仕様(CSS Transforms Module)によると:

    "For elements whose layout is governed by the CSS box model, any value other than none for the transform also causes the element to establish a containing block for all descendants."

    訳:
    transformにnone以外の値を指定すると、その要素はすべての子孫要素に対して「包含ブロック(containing block)」となる。

    つまり、position: fixedの基準点が、ビューポートから親要素に変更されるのです。


    4. 実際に起きる問題のデモ

    ケーススタディ: 背景アニメーション + 固定ヘッダー

    よくあるシナリオを見てみましょう。

    <div class="background-animation">
      <!-- アニメーション背景 -->
      <header class="fixed-header">
        固定ヘッダー
      </header>
      <main>
        メインコンテンツ
      </main>
    </div>
    

    問題が起きるコード

    /* 背景アニメーション用の最適化 */
    .background-animation {
      position: relative;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      animation: gradientShift 10s infinite;
      
      /* パフォーマンス最適化のつもりで追加 */
      will-change: background-position;
      transform: translateZ(0); /* ← これが原因! */
    }
    
    /* 固定ヘッダー */
    .fixed-header {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      background: rgba(255, 255, 255, 0.9);
      z-index: 100;
      /* スクロールしても.background-animationの上部に貼り付く
         (本来は画面上部に固定されるべき) */
    }
    

    結果:

    • ヘッダーが画面に固定されない
    • スクロールすると一緒に動いてしまう
    • z-indexを上げても解決しない

    z-indexが効かない問題

    さらに、z-index: -1を::beforeや::afterに指定している場合も問題が複雑になります。

    .background-animation::before {
      content: '';
      position: absolute;
      z-index: -1; /* 背景を後ろに配置したいだけなのに... */
      /* これも新しいスタッキングコンテキストに影響する */
    }
    

    5. 解決策と代替手法

    解決策1: will-changeとtransformを削除

    最もシンプルな解決法は、問題の原因を取り除くことです。

    .background-animation {
      position: relative;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      animation: gradientShift 10s infinite;
      
      /* will-changeとtransformを削除 */
      /* will-change: background-position; */
      /* transform: translateZ(0); */
    }
    

    メリット:

    • position: fixedが正常に動作
    • コードがシンプル

    デメリット:

    • 複雑なアニメーションでパフォーマンスが落ちる可能性

    解決策2: 疑似要素に最適化を移動

    親要素からwill-changetransformを削除し、代わりに::before::afterに適用します。

    疑似要素は子要素に影響しません。

    .background-animation {
      position: relative;
      /* will-changeとtransformは削除 */
    }
    
    /* 疑似要素で背景エフェクトを作成 */
    .background-animation::before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: -1;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      animation: gradientShift 10s infinite;
      
      /* 疑似要素にのみ最適化を適用 */
      will-change: transform, opacity;
      transform: translateZ(0);
    }
    
    .fixed-header {
      position: fixed;
      top: 0;
      /* 正常に動作する! */
    }
    

    メリット:

    • パフォーマンス最適化を維持
    • position: fixedが正常に動作
    • 構造が明確

    デメリット:

    • HTMLの構造変更が必要な場合がある

    解決策3: HTML構造を変更

    アニメーション要素と固定要素を別の親要素に分ける方法です。

    <!-- 背景レイヤー -->
    <div class="background-layer">
      <!-- アニメーション背景 -->
    </div>
    
    <!-- コンテンツレイヤー -->
    <div class="content-layer">
      <header class="fixed-header">
        固定ヘッダー
      </header>
      <main>
        メインコンテンツ
      </main>
    </div>
    
    .background-layer {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: -1;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      animation: gradientShift 10s infinite;
      
      /* ここでは最適化OK */
      will-change: background-position;
      transform: translateZ(0);
    }
    
    .content-layer {
      position: relative;
      z-index: 1;
      /* will-changeとtransformなし */
    }
    
    .fixed-header {
      position: fixed;
      top: 0;
      /* 正常に動作する! */
    }
    

    メリット:

    • 最も確実
    • レイヤー構造が明確

    デメリット:

    • HTMLの変更が必要
    • 要素が増える

    6. いつ使うべきか、いつ避けるべきか

    will-changeを使うべき場合

    • ユーザーのインタラクションでアニメーションが開始する直前
    • 複雑なtransformやopacityのアニメーション
    • 60fpsを維持する必要がある場合
    .button {
      transition: transform 0.3s;
    }
    
    .button:hover,
    .button:focus {
      will-change: transform;
      /* ホバー時のみ有効化 */
    }
    
    .button:not(:hover):not(:focus) {
      will-change: auto;
      /* 使わない時は解除 */
    }
    

    will-changeを避けるべき場合

    • すべての要素にデフォルトで適用
    • 子要素にposition: fixedがある親要素
    • シンプルなアニメーション(colorやbackground-colorなど)
    /* 悪い例 */
    * {
      will-change: transform, opacity;
      /* メモリを大量消費 */
    }
    

    transform: translateZ(0)を使うべき場合

    使うべき:

    • 疑似要素(::before, ::after)
    • position: fixedの子要素がない場合
    • レガシーブラウザ対応が必要な場合

    避けるべき:

    • position: fixedの子要素がある親要素
    • デフォルトでパフォーマンスが十分な場合
    • モダンブラウザのみをターゲットにする場合

    7. まとめ

    重要なポイント

    1. will-changetransformは新しいスタッキングコンテキストを作成する

      • これにより、子要素のposition: fixedの基準点が変わる
    2. 疑似要素(::before, ::after)は子要素に影響しない

      • 最適化をここに移動するのが効果的
    3. パフォーマンス最適化は必要な時だけ

      • 「とりあえず」でwill-changeを使うのは避ける
    4. 問題が起きたら構造を見直す

      • HTML/CSSの構造を変更することで根本的に解決

    チェックリスト

    以下の条件に当てはまる場合は注意:

    • 親要素にwill-change: transformがある
    • 親要素にtransform: translateZ(0)translate3d(0,0,0)がある
    • 子要素でposition: fixedを使っている
    • 固定要素が画面ではなく親要素を基準に配置されている
    • z-indexを上げても解決しない

    参考リソース

    パフォーマンス最適化は強力なツールですが、副作用を理解して使うことが重要です。