このページの中身

性能と配慮

動かしてよいのは transform と opacity です。ほかを動かすと、1コマに使える約16.7msを毎回レイアウトの計算に食われます。

ブラウザの描画工程と、どのCSSプロパティがどの工程から作り直すかを示した図
図を大きく見る
図の中身を文字で読む
  • 1フレームは約16.7ms。この中で終わらない処理を毎フレーム求めると、コマが落ちる。
  • スタイル計算 レイアウト ペイント 合成(GPU) → 画面
  • width / height / top / left を動かす レイアウトからやり直し。毎フレーム再計算になり、いちばん重い
  • background-color / box-shadow / border-radius を動かす ペイントからやり直し
  • transform / opacity を動かす 合成だけで済む
  • 言い換えると 位置を動かすなら left ではなく translate、大きさなら width ではなく scale、消すなら display ではなく opacity。
  • will-change は合成用の層を先に作らせる指示。乱発すると層が増えてメモリを圧迫するため、動く直前に付けて、終わったら外す。
  • スクロール駆動アニメーションは CSS で書けばメインスレッドを使わない。JavaScript のスクロールイベントで同じことをすると引っかかる。

ひと目で#

フレームレート
1秒あたりのコマ数。60fpsなら1コマに使えるのは約16.7ms
ジャンク
間に合わずにコマが落ちること。引っかかりとして感じられる
コンポジタプロパティ
transform と opacity。GPU側の合成だけで済む
スクロール駆動アニメーション
スクロールの位置を進行度に使う。CSSで書けばメインスレッドを使わない
prefers-reduced-motion
動きを減らす設定。読み取って従う

使い分け#

やりたいこと✕ 使わない○ 使う
位置を動かすleft top margintransform: translate()
大きさを変えるwidth heighttransform: scale()
消す、出すdisplayopacity
回す画像の差し替えtransform: rotate()
スクロールに連動させるscroll イベントスクロール駆動アニメーション

影とぼかしは、完成形を先に描いておく#

transform と opacity の2つしか動かせないと聞くと、影が濃くなるホバーは作れないように思えます。影のほうを動かすのではなく、影付きの層を先に作って透明度で出し入れします。

box-shadowを直接動かした場合と、影を疑似要素に持たせてopacityを動かした場合の、1コマごとの仕事量を比べた図
図を大きく見る
図の中身を文字で読む
  • ✕ box-shadow をそのまま動かす ○ 影は疑似要素に持たせ、opacity を動かす
  • .card:hover { box-shadow: 0 8px 24px …; } .card::after { box-shadow: …; opacity: 0; }
  • .card:hover::after { opacity: 1; }
  • 影の形がコマごとに変わる
  • 影は最初に1回だけ描かれる
  • 1コマごとの仕事 1コマごとの仕事
  • 毎コマ、影を描き直す(描画が走る) 合成で透け具合を変えるだけ
  • 動かしてよいのは transform と opacity。影・ぼかし・グラデーションは、完成形を先に描いておいて透明度で切り替える。

box-shadow を直接補間すると、ブラウザは毎コマ影を描き直します。疑似要素に影を持たせておけば、描くのは最初の1回だけで、あとは合成が透け具合を変えるだけです。ぼかしやグラデーションも同じ考え方で置き換えられます。

このコードで動いている見本です

動きを減らす設定への対応#

全部を止めると、状態が変わったこと自体が伝わらなくなります。大きな移動、回転、視差、自動再生は止め、色と不透明度の変化は残すのが実務での落としどころです。

間違えやすいところ#

並べて動かす#

同じ移動を、left で動かす箱と transform で動かす箱で同時に再生します。見た目はほぼ同じでも、ブラウザの中の仕事が違います。

left
transform

left を動かすと、ブラウザは毎コマ位置の計算(レイアウト)からやり直します。transform は描き終えた絵を合成でずらすだけなので、1コマ約16.7msの予算を食いません。

DevTools での測り方

  1. F12 で開発者ツールを開き、Performance パネルで記録を始めます。
  2. 上で「left だけ」を選んで「くり返す」を押し、3秒ほど録って止めます。Main の帯に紫(レイアウト)が毎コマ並びます。
  3. 「transform だけ」に替えて同じ記録を取ります。紫が消え、合成だけで済むことを確かめられます。
:root { --ease-out: cubic-bezier(0.16, 1, 0.3, 1); }

/* 位置は transform で動かす */
.box { transition: transform 600ms var(--ease-out); }
.box.is-moved { transform: translateX(240px); }

/* 避ける: left を動かすと毎コマ、レイアウトの再計算が走る */
/* .box { position: relative; transition: left 600ms; } */

@media (prefers-reduced-motion: reduce) {
  .box { transition-duration: 0.01ms; }
}

この道具は JavaScript で動きます。無効の場合は、下のコードの transform の書き方を使ってください。

実装#

このコードで動いている見本です

確かめ方#

同じ移動を left と transform で記録すると、帯の色が変わります。

開発者ツール Performance パネルで、leftで動かした記録とtransformで動かした記録を上下に並べた実測
図を大きく見る

left の側は紫(レイアウト)が毎コマ並び、コマ落ちの赤い印が付きます。transform の側は緑(合成)だけで済みます。CPU を4〜6倍遅くして記録すると、手元の端末でも差が出ます。

直す例#

架空の案件を1つ例にとります。どこを見て、何をどう直すかを書きます。

イベントサイトの固定ヘッダーです。スクロールに合わせて top を書き換え、影の広がりも box-shadow で連続的に変えています。手元の PC では滑らかに見えますが、来場者の多くが使う数世代前の Android では、スクロールが引っかかります。

top は位置なので、動かすとレイアウトの計算からやり直しになります。box-shadow は影の形そのものが変わるので、毎コマ描き直しです。1コマに使える約16.7msを、この2つで食い切っています。

置き換えは2つです。位置は transform: translateY() に。影は疑似要素に置いて opacity で出し入れします。どちらも合成だけで済むので、レイアウトも描画も走りません。CPU を4倍遅くして Performance で記録すると、紫の帯が消えるところまで見えます。

確認する#

使えるパターン#

この話が要る場面#

このページが答えになる、現場の困り事です。困り事の側から探すと、同じ場所に着きます。

動きがカクつく

left や width を動かしている。transform と opacity に替える。

このコードで動いている見本です

スクロールに合わせた演出が重い

scrollイベントで毎回DOMを読んでいる。CSSのスクロール駆動アニメーションかIntersectionObserverに替える。

このコードで動いている見本です

will-change を付けたのに速くならない

常時付けると層が増えてかえって重い。動く直前に付けて、終わったら外す。

このコードで動いている見本です

この場面のほかの困り事と、そのまま貼れる部品は動かすにまとめてある。逆引きには全188件。

出典#

関連#

この資料は、Webサイトを作るときに引くためのものです。 何のために作ったか。作っているのは AK²Lab。