やあ。今日もどこかのコードレビューで「アニメーションがカクつくから何とかしてくれ」って泣きついてこられたところかい?よくある話だよね。スタイルの崩れやメモリリークも厄介だけど、一番ユーザーにストレスを与えるのは、やっぱり画面の「ガタつき(Jank)」なんだ。
画面がスムーズに60fps(あるいは120fps)で動くかどうかは、DOMやCSSのコード量だけじゃなくて、「ブラウザが裏側でピクセルをどう描画し、どう重ね合わせているか」を理解しているかにかかっている。
今回は、レイアウトが終わった後の最後の関門、「ペイント(Paint)」と「コンポジット(Compositing:レイヤー合成)」の深淵について、実務で明日から使える知見を交えて徹底的に解説しよう。
—
1. そもそも「ペイント」と「コンポジット」で何が起きているのか?
DOMとCSSOMが合体した「レンダーツリー」ができあがり、それぞれの要素が「どこに配置されるか(レイアウト/リフロー)」が決まった後、ブラウザは本格的に画面を作る作業に入る。ここからが本番だ。
ペイント(Paint / Rasterization)の正体
ペイントとは、ざっくり言うと「要素を塗る作業」だ。
背景色、テキストの色、ボーダー、シャドウなど、「この要素はこういう見た目ですよ」という情報を、ピクセル単位ではなく「ここに線を描け、ここに塗りを入れろ」という描画命令のリスト(Display List)として生成する工程を指す。
そして、この描画命令を実際のビットマップ(ピクセルデータ)に変換する作業をラスタライズ(Rasterization)と呼ぶ。以前のブラウザはメインスレッドが全部これをやっていて大忙しだったけれど、現代のモダンブラウザ(BlinkやGecko)は、ここを複数のタイルに分割して、GPUのパワーを使って非同期で処理(ラスタライズ)している。
コンポジット(Compositing)の魔術
ペイントが終わったら、次はそれを画面に映し出す必要がある。ここで登場するのがコンポジット(レイヤー合成)だ。
Webページというのは、一枚のキャンバスにすべてを描いているわけじゃない。Photoshopのレイヤー構造を思い出してほしい。ブラウザは特定の条件を満たした要素を、メインのキャンバスとは別の独立した「レイヤー(Composite Layers)」として切り離し、それぞれ個別にペイント・ラスタライズさせる。
そして、最終的にそれらのレイヤーをGPU上で「ガッチャンコ」して一つの画面にする。これがコンポジットの仕組みだ。
なぜこんな面倒なことをするのか?答えはシンプルで、「一部のレイヤーだけを動かす(移動・拡大縮小・透明度変更)なら、下のレイヤーを再ペイントしなくていいから爆速になる」からに他ならない。
—
2. ブラウザの裏側:レイヤーが昇格(Promote)する条件
すべての要素が別レイヤーになると、GPUのメモリ(VRAM)がパンクしてしまう。だからブラウザは、デフォルトではドキュメント全体を基本的に一つのレイヤー(あるいはごく少数のレイヤー)として扱う。
しかし、特定のCSSプロパティや条件が指定されたとき、ブラウザは「おっ、こいつは激しく動く気だな?单独のレイヤーにしてやろう」と判断し、レイヤーを昇格(Promote)させる。これを実務では「ハードウェアアクセラレーションの有効化」と呼ぶこともあるね。
レイヤーが昇格する主なトリガーを知っておこう:
1. `transform: translate3d()`, `translateZ()`, `matrix3d()` などの3D・特定2Dトランスフォーム
2. `opacity` がアニメーションしている要素(JSやCSS transitions/animations経由)
3. `will-change` プロパティに `transform` や `opacity` が指定されている場合
4. `
特に `will-change` は、「これから動かすぞ」という合図を事前にブラウザに送ることで、ペイントのタイミングを最適化できる強力なプロパティだ。ただし、乱用するとVRAMを食いつぶして逆にパフォーマンスが落ちる諸刃の剣だから注意してくれよ。
—
3. 実務で直面する「やってはいけないアンチパターン」
現場でよく見かけるパフォーマンス劣化の原因はだいたい決まっている。以下のコードを見てほしい。
/ 【アンチパターン】レイアウトとペイントを毎フレーム強制する例 /
.bad-animation {
position: absolute;
left: 0;
top: 0;
transition: left 0.3s ease, top 0.3s ease;
}
.bad-animation:hover {
left: 100px;
top: 100px;
}
このコードの何が問題か分かるかい?
`left` や `top` などのプロパティをアニメーションさせると、ブラウザは毎フレームごとに「レイアウト(再計算) ➔ ペイント ➔ コンポジット」のフルコース(パイプライン)を強制されることになる。メインスレッドが常にフル稼働し、カクつき(Jank)の温床になるんだ。
—
4. 現場で使える!極上の最適化テクニックとサンプルコード
じゃあ、どう書くのが正解なのか。答えは「コンポジットだけで処理できるプロパティ(`transform` と `opacity`)だけを使うこと」だ。これらを使えば、メインスレッドをバイパスして、GPUだけでアニメーションを完結させられる(レイアウトもペイントもスキップできる!)。
以下のサンプルコードを見てほしい。これが、実務で私たちが採用しているモダンかつ最もパフォーマンスの高いアプローチだ。
シニアエンジニアの知恵袋
このカードは transform と will-change を駆使して実装されています。ブラウザのメインスレッドを一切ブロックせず、GPUのコンポジットレイヤーだけで滑らかに浮き上がります。
このコードのポイント
1. `transform: translateY()` の採用:位置の移動に `top` ではなく `transform` を使うことで、ブラウザのレンダリングパイプラインから「Layout(Reflow)」と「Paint」の工程をごっそり消し去っている。
2. `will-change` の適切な利用:ブラウザに先回りしてレイヤーを作らせることで、マウスホバーした瞬間の「初動の引っかかり(Jank)」を完全に排除している。
—
5. チーフアーキテクトからの実践的なアドバイス
ブラウザのレンダリングの仕組みを知っていると、デバッグのスピードが劇的に変わる。何か画面が重いなと感じたら、Chrome DevToolsの 「Performance」タブ や、「Layers」タブ を開く癖をつけよう。
- Layersタブを見れば、今どの要素が無駄に別レイヤーに昇格してVRAMを圧迫しているか、あるいは逆に意図せず単一レイヤーになっていて毎回ペイントが走っているかが一目瞭然だ。
- Renderingタブの「Paint flashing(ペイント点滅)」にチェックを入れて画面を触ってみるといい。アニメーションのたびに画面全体が緑色にペカペカ光っていたら、それは「無駄なペイントが走りまくっている証拠」だ。直ちにコードを見直すべきシグナルだよ。
Webブラウザは優秀だけど、魔法の箱じゃない。私たちが書いたCSSの性質を読み解き、裏側でどういう計算をさせられているかを想像しながらコードを書くこと。それこそが、ジュニアからシニアへステップアップするための最大の近道なんだ。
さあ、今日のコードから無駄な `top` や `left` のアニメーションを根絶やしにしようぜ!

コメント