よう、元気にしてるか?
今日のテーマは、Webブラウザのレンダリングパイプラインの中でも、特に「リペイント」に焦点を当てていこう。フロントエンドのパフォーマンスチューニングを語る上で、リフロー、リペイント、そしてコンポジット合成は避けて通れない三大巨頭だ。この三つの仕組みを肌感覚で理解しているかどうかが、君たちの書くコードのパフォーマンスを大きく左右する。
今回はその中でも、比較的「軽い」とされがちなリペイント。だが、「軽いからといって油断は禁物」という、現場のリアルな泥臭さを交えながら、リペイントを引き起こすプロパティの正体と、ブラウザが裏側でどう処理しているのかを、徹底的に解き明かしていく。
君たちが「なるほど!」と膝を打つような、実践的な知見と、すぐに使えるサンプルコードを準備した。さあ、Webブラウザの深淵を覗いてみようじゃないか。
—
リペイント、その名の裏に隠された真実
まず、この話を始める前に、リフロー、リペイント、コンポジット合成というブラウザの描画処理の基本的な流れを軽くおさらいしておこう。
1. Style (スタイル計算): DOMツリーとCSSOMツリーを合わせて、各要素に最終的に適用されるスタイルを計算する。
2. Layout (レイアウト計算、リフロー): 各要素のスタイルが確定したら、ブラウザはその要素の正確な位置とサイズを計算する。これが「リフロー」だ。要素の形状や配置が変わると、このフェーズが走る。コストが非常に高い処理だ。
3. Paint (描画、リペイント): レイアウトが確定したら、要素の見た目(色、影、画像など)をピクセルとして「描き出す」作業に入る。これが「リペイント」だ。
4. Composite (合成): 描画されたレイヤーを重ね合わせ、最終的な画面を生成する。
今日の主役である「リペイント」は、つまり「Paint」フェーズで発生する処理のことだ。要素のレイアウト(位置やサイズ)は変わらないけれど、その「見た目」だけが変わる場合に引き起こされる。リフローに比べれば確かに軽い。だが、それが本当に「無視できるほど軽い」のかどうかは、常に疑ってかかるべきだ。特に、アニメーションや頻繁なDOM操作を伴うケースでは、この「軽い」はずのリペイントが積み重なって、ユーザー体験を著しく損ねる原因になることも少なくない。
リフロー vs リペイント: 何が違うのか?
これは君たち中級エンジニアならもう知っているだろうが、改めてその本質を理解しておこう。
- リフロー(Layout): 要素の幾何学的情報(幅、高さ、位置など)が変更されたときに発生する。これにより、関連する他の要素のレイアウトも再計算される可能性がある。例えば `width`, `height`, `margin`, `padding`, `display`, `position` などがこれに当たる。ドミノ倒しのように、広範囲に影響が及ぶため、非常にコストが高い。
- リペイント(Paint): 要素の幾何学的情報には影響を与えず、見た目の変更のみが行われる場合に発生する。例えば `color`, `background-color`, “visibility` などがこれに当たる。影響範囲は通常、その要素とその子孫要素に限定されるため、リフローよりはコストが低い。
今日のテーマは、この「リフローを引き起こさず、リペイントのみを引き起こすプロパティ」について深掘りしていく。
—
リペイントを引き起こすプロパティたち
さて、本題だ。具体的にどんなCSSプロパティがリペイントを引き起こすのか、そしてブラウザがその裏側でどう処理しているのかを見ていこう。
1. `background-color` と `color`
これは最も典型的で分かりやすい例だ。
- プロパティ: `background-color`, `color`, `text-decoration`, `border-style`, `border-width`以外の`border`プロパティ(例: `border-color`)など。
- ブラウザの裏側: ブラウザのレンダリングエンジンは、要素のボックスモデル(幅や高さ、余白など)をすでに計算し終えている。`background-color`や`color`の変更は、その確定したボックスの「中身」や「表面」の色情報だけを更新する。つまり、要素のサイズや位置を再計算する必要は一切ない。単に、そのピクセルを新しい色で上書きすればいいだけなので、Paintフェーズだけで完結するわけだ。
例えば、ボタンのホバーエフェクトで背景色が変わるような場合、これはリペイントで処理される。
2. `visibility`
`display: none` と `visibility: hidden` の違いは、もはや古典的な面接問題にもなるが、パフォーマンスの観点から見ると非常に重要だ。
- プロパティ: `visibility` (`visible`, `hidden`, `collapse`)
- ブラウザの裏側:
- `display: none` は要素を完全にDOMツリーから削除し、その空間も占有しない。これは周辺要素のレイアウトに影響を与えるため、リフローを誘発する。
- `visibility: hidden` は要素を非表示にするが、その空間は占有したままだ。つまり、レイアウトは一切変わらない。ブラウザは、その要素が占めていた領域を「透明なもの」として再描画するだけで済む。だから、これはリペイントで処理される。
アニメーションで要素をフェードアウトさせたいが、レイアウトは崩したくない場合に `opacity: 0` と `visibility: hidden` を組み合わせて使うのは、まさにこのリペイントの特性を活かしたテクニックだ。
3. `outline` と `box-shadow`
要素の輪郭や影も、レイアウトには影響しない描画の範疇だ。
- プロパティ: `outline`, `box-shadow`, `text-shadow`
- ブラウザの裏側: `outline`は要素のボックスの外側に描画され、他の要素のレイアウトに影響を与えない。`box-shadow`や`text-shadow`も同様で、要素の境界線の外側に広がる「描画」であり、要素自体のサイズや位置を変えるものではない。そのため、これらもリフローを発生させずにリペイントで処理される。
ただし、`box-shadow`や`text-shadow`は、その計算コストが意外と高い場合がある。特に複雑な影や多数の要素に適用すると、描画負荷が無視できなくなることもあるので注意が必要だ。単なる色変更とは一線を画する。
4. `border-radius`
要素の角を丸くする`border-radius`も、見た目の変更だがレイアウトには影響しない。
- プロパティ: `border-radius`
- ブラウザの裏側: 要素の矩形の「形状」を描画時に変更するだけで、その要素が占める空間の幅や高さ、位置は一切変わらない。これも純粋な描画の変更なので、リペイントで処理される。
5. `opacity`
透明度を調整する`opacity`も、レイアウトには影響しない。だが、これは少し面白い特性を持っている。
- プロパティ: `opacity`
- ブラウザの裏側: 現代のブラウザでは、`opacity`の変更は多くの場合、リペイントではなく「コンポジット合成」フェーズで処理されるように最適化されている。特に、`opacity`をアニメーションさせる場合、ブラウザはその要素を独立した「コンポジットレイヤー」に昇格させ、GPUを使って高速に合成する。これにより、CPUでのリペイントをスキップし、よりスムーズなアニメーションが可能になる。
しかし、これは「常に」コンポジット合成で処理されるわけではない。要素の構造や他のCSSプロパティとの兼ね合いによっては、依然としてリペイントを誘発するケースも存在する。このあたりのブラウザの最適化ロジックは非常に複雑で、バージョンやブラウザの実装によっても異なる場合があるから、デベロッパーツールで確認する癖をつけておくのが賢明だ。
リペイントを引き起こすプロパティ一覧(例)
- `background-color`
- `background-image`
- `background-position`
- `background-repeat`
- `background-size`
- `border-color`
- `border-style`
- `border-radius`
- `box-shadow`
- `color`
- `cursor`
- `font-family`
- `font-size` (ただし、`font-size`はテキストの幅に影響する場合があるので、リフローも起こり得る)
- `font-style`
- `font-weight`
- `line-height` (これもテキストの高さに影響するため、リフローも起こり得る)
- `list-style`
- `outline`
- `outline-color`
- `outline-style`
- `outline-width`
- `text-decoration`
- `text-shadow`
- `visibility`
このリストは網羅的ではないが、傾向として「要素のボックスモデル(位置やサイズ)を変えない、純粋な見た目の変更」がリペイントの対象となる。
—
パフォーマンスへの影響と最適化のヒント
「リペイントはリフローより軽い」というのは事実だが、だからといって無頓着でいていいわけじゃない。特に、以下のようなケースではリペイントがパフォーマンスのボトルネックになる可能性がある。
- 広範囲なリペイント: 画面全体を覆うような大きな要素、あるいは多くの要素が同時にリペイントされる場合。
- 複雑な描画: `box-shadow`や`gradient`、`filter`プロパティなど、描画計算コストが高いスタイルを多用している場合。
- 頻繁なリペイント: アニメーションやスクロールイベントなどで、毎フレームのようにリペイントが発生する場合。
では、どうすればこのリペイントのコストを最適化できるのか?
1. スタイルの変更は最小限に
これは基本中の基本だ。JavaScriptでDOM要素のスタイルを直接操作する際は、不必要な変更は避け、影響範囲を最小限に抑えるよう意識しよう。`element.style.property = ‘value’` のような直接操作よりも、CSSクラスを切り替える方が、変更がまとめて適用されるため効率的だ。
// BAD: スタイルを一つずつ変更すると、その都度リペイントが発生する可能性がある
// (ブラウザの最適化でまとめて処理されることもあるが、保証はない)
element.style.backgroundColor = ‘red’;
element.style.color = ‘white’;
element.style.textDecoration = ‘underline’;
// GOOD: クラスを切り替えることで、ブラウザは変更を一度に処理しやすくなる
element.classList.add(‘active-state’);
/ CSS /
.active-state {
background-color: red;
color: white;
text-decoration: underline;
}
2. `will-change` プロパティの活用
これはブラウザに「この要素は今後アニメーションするかもしれないから、事前に最適化しておいてね」とヒントを与えるプロパティだ。
.animated-element {
will-change: opacity, transform; / opacityとtransformが変化する可能性があることをブラウザに伝える /
}
`will-change`を使うことで、ブラウザはその要素を事前に独立したコンポジットレイヤーに昇格させ、GPUによるコンポジット合成で処理できるように準備する。これにより、アニメーション開始時のリペイントやリフローを抑制し、スムーズなアニメーションを実現できる。
ただし、`will-change`の濫用は厳禁だ。すべての要素に指定すると、逆にメモリを大量消費したり、GPUリソースを食いつぶしたりして、パフォーマンスを悪化させる可能性がある。本当にアニメーションが予定されている、限られた要素にのみ賢く使おう。アニメーションが終わったら、プロパティを削除することも検討すべきだ。
3. デベロッパーツールで確認する習慣をつける
Chromeのデベロッパーツールの「Rendering」タブにある「Paint flashing」は、リペイントが発生している領域を緑色でハイライト表示してくれる。これはまさに現場で役立つ最強のツールだ。自分の書いたコードがどこで、どれくらいの頻度でリペイントを引き起こしているのか、この目で確認する習慣をつけよう。

(画像はイメージです。実際にはデベロッパーツールで表示される緑色のハイライト)
—
現場で使える!リペイントを理解するサンプルコード
さあ、口で説明するだけではつまらない。実際にコードを動かして、リペイントがどう発生しているのか、デベロッパーツールで確認してみよう。
以下のHTMLファイルを保存し、ブラウザで開いてみてほしい。
- リペイント、その名の裏に隠された真実
- リフロー vs リペイント: 何が違うのか?
- リペイントを引き起こすプロパティたち
- 1. `background-color` と `color`
- 2. `visibility`
- 3. `outline` と `box-shadow`
- 4. `border-radius`
- 5. `opacity`
- リペイントを引き起こすプロパティ一覧(例)
- パフォーマンスへの影響と最適化のヒント
- 1. スタイルの変更は最小限に
- 2. `will-change` プロパティの活用
- 3. デベロッパーツールで確認する習慣をつける
- 現場で使える!リペイントを理解するサンプルコード
- リペイントを誘発するプロパティの変化を見てみよう
リペイントを誘発するプロパティの変化を見てみよう
デベロッパーツールを開き、「Rendering」タブの「Paint flashing」をオンにしてみよう。
ボタンをクリックすると、要素が変更され、リペイントが発生する領域が緑色にハイライトされるはずだ。
試してみよう!
1. このHTMLをファイルとして保存し、Chromeなどのブラウザで開く。
2. 開発者ツール(F12キーや右クリック→「検証」)を開く。
3. 開発者ツールのドックを切り替えて、「Rendering」タブを見つける。(もし見つからなければ、開発者ツール右上の三点リーダーメニューから「More tools」→「Rendering」を選択)。
4. 「Rendering」タブの中にある「Paint flashing」にチェックを入れる。
5. Webページ上の各ボタンをクリックしてみよう。要素が変更されるたびに、その領域が緑色にハイライトされるはずだ。これが、ブラウザがリペイント処理を行っている証拠だ。
特に「色・下線変更」や「影を変更」は、純粋なリペイントであることがよく分かるだろう。「透明度 (Opacity)」も緑色に光る場合があるが、これはブラウザの最適化(コンポジットレイヤーへの昇格)が効いていないか、または部分的なリペイントを伴っているケースだ。
—
最後に:ブラウザの仕組みを知ることは「武器」になる
今日の話は、リペイントという特定の描画フェーズに絞ったものだったが、重要なのは、君たちがブラウザの内部挙動に常に好奇心を持ち、なぜそうなるのか、どうすればもっと良くできるのかを考え続けることだ。
リペイントはリフローほど「重い」処理ではないかもしれない。だが、アニメーションやインタラクティブなUIでは、この「軽い」はずのリペイントが積み重なって、ユーザー体験を損ねる元凶となることが往々にしてある。目に見えないが故に、見過ごされがちなパフォーマンスの落とし穴だ。
デベロッパーツールを片手に、自分のコードがブラウザにどう解釈され、どう描画されているのかを常に探求する姿勢。それが、世界最高峰のフロントエンドエンジニアになるための第一歩だ。今日の話が、君たちのコードに少しでも良い影響を与えられれば、これ以上の喜びはない。
また何か困ったことがあれば、いつでも声をかけてくれ。
じゃあな!

コメント