性能と配慮
動かしてよいのは transform と opacity です。ほかを動かすと、1コマに使える約16.7msを毎回レイアウトの計算に食われます。
図の中身を文字で読む
- 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 margin | transform: translate() |
| 大きさを変える | width height | transform: scale() |
| 消す、出す | display | opacity |
| 回す | 画像の差し替え | transform: rotate() |
| スクロールに連動させる | scroll イベント | スクロール駆動アニメーション |
影とぼかしは、完成形を先に描いておく#
transform と opacity の2つしか動かせないと聞くと、影が濃くなるホバーは作れないように思えます。影のほうを動かすのではなく、影付きの層を先に作って透明度で出し入れします。
図の中身を文字で読む
- ✕ 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で位置を動かすレイアウトから作り直しになり、毎コマ再計算が走るbox-shadowを動かす描画が重い。影を持つ疑似要素のopacityで代えるwill-changeを常に付けておく合成用の層が増え続けて、メモリを圧迫する- スクロールイベントで毎回 DOM を読むレイアウトの再計算が同期で走る
- 速い端末だけで確かめる引っかかりは遅い端末で出る
prefers-reduced-motionで全部止める変化が伝わらなくなる- 動きを止めた状態で成り立たない画面を作る止めたときに情報が欠ける
並べて動かす#
同じ移動を、left で動かす箱と transform で動かす箱で同時に再生します。見た目はほぼ同じでも、ブラウザの中の仕事が違います。
left を動かすと、ブラウザは毎コマ位置の計算(レイアウト)からやり直します。transform は描き終えた絵を合成でずらすだけなので、1コマ約16.7msの予算を食いません。
DevTools での測り方
- F12 で開発者ツールを開き、Performance パネルで記録を始めます。
- 上で「left だけ」を選んで「くり返す」を押し、3秒ほど録って止めます。Main の帯に紫(レイアウト)が毎コマ並びます。
- 「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 で記録すると、帯の色が変わります。

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 で記録すると、紫の帯が消えるところまで見えます。
確認する#
- 動かしているのが
transformとopacityだけ(topwidthbox-shadowが残っていない) will-changeが動く直前に付き、終わったら外れている- CPU を4〜6倍遅くした記録で、コマ落ちの印が出ない
使えるパターン#
この話が要る場面#
このページが答えになる、現場の困り事です。困り事の側から探すと、同じ場所に着きます。
left や width を動かしている。transform と opacity に替える。
このコードで動いている見本です
scrollイベントで毎回DOMを読んでいる。CSSのスクロール駆動アニメーションかIntersectionObserverに替える。
このコードで動いている見本です
常時付けると層が増えてかえって重い。動く直前に付けて、終わったら外す。
このコードで動いている見本です
この場面のほかの困り事と、そのまま貼れる部品は動かすにまとめてある。逆引きには全188件。