【実務・中級編】 containプロパティによるレンダリングの分離 – Webブラウザの仕組み実践ガイド

やあ。今日も今日とて、メモリを爆食いするブラウザの機嫌を取りながらコードを書いているかい?

フロントエンドをやっていると、一度は直面するんだよね。「なんだかこのモーダルを開閉するたびに、画面全体がカクつくぞ」とか、「無限スクロールのリストで、新しいアイテムを読み込むたびにメインスレッドがフリーズする」という悪夢に。

大抵のエンジニアは、ここで「コンポーネントの分割が足りないのか?」とか「Reactの再レンダリングが……」なんて言い出して、不要な `useMemo` や `useCallback` のお札を貼りまくる。気持ちは痛いほど分かる。でもね、ちょっと待ってほしい。それ、JavaScriptのレイヤーで頑張る前に、ブラウザのレンダリングパイプラインの仕組みを理解して、適切な指示を出せば一発で解決することが多いんだ。

今回は、そんなブラウザの重たい腰を軽くするための秘密兵器、CSSの `contain` プロパティについて話をしよう。実務で即座に使える実践的な知見を叩き込むから、コーヒーでも飲みながら聞いてくれ。

—

なぜブラウザは「たった一つの変更」で全力を出してしまうのか?

まず、敵を知るためにブラウザの裏側の話をしよう。
君たちが普段書いているHTMLとCSSが画面に映し出されるまでには、いくつかの残酷なステップがある。

1. HTMLのパース & DOMツリー構築
2. CSSのパース & CSSOMツリー構築
3. レンダーツリー構築
4. レイアウト(Layout / Reflow):各要素の正確な位置とサイズを計算する
5. ペイント(Paint):ピクセルを塗りつぶす
6. コンポジット(Composite):レイヤーを合成して画面にする

ここで問題になるのが、4番目の「レイアウト(Reflow)」だ。
DOMの一部――例えば、画面の端っこにある小さな通知バッジのサイズが1ピクセル変わったとする。素朴なブラウザの心構えとしてはこうだ。

「おい! バッジの大きさが変わったぞ! ひょっとすると、これが親要素を押し広げて、そのまた親要素を揺らし、最終的にページ全体のレイアウトに影響するかもしれない! 安全のために、ドキュメントのてっぺんから一番下まで、全部の要素のサイズと位置を再計算し直すぞ!」

……いやいや、ちょっと待てブラウザ君。その通知バッジ、親のレイアウトから完全に独立してる孤島なんだよ。全体の計算なんて必要ないだろ!

このブラウザの「過剰なまでの真面目さ(全域再計算)」を強制的に食い止め、「ここから先は影響しないから、このサブツリーの中だけで完結させてくれ!」とブラウザに約束させる魔法の呪文。それが `contain` プロパティだ。

—

`contain` プロパティがもたらす4つの独立性

`contain` は、要素に対して「このサブツリーは、ページ内の他の部分からどれくらい独立しているか」をブラウザに教えるプロパティだ。これによって、ブラウザは無駄なレイアウトやペイントの範囲をバッサリと切り捨てる(スコープ化する)ことができる。

指定できる主な値は以下の4つだ。

1. `layout`: 要素の内部レイアウトが、外部の要素に影響を与えず、また外部からも影響を受けないことを保証する。
2. `paint`: 要素の内部が、その境界の外側に描画されない(はみ出さない)ことを保証する。これにより、この要素が画面外や隠れている場合、ペイント処理自体を丸ごとスキップできる。
3. カプセル化されたスタイル(`style`): カウンターやCSSスコープの汚染を防ぐ(※あまりレイアウトパフォーマンス直結では使われない)。
4. `size`: 要素のサイズ計算に、その子要素のサイズを考慮しなくてもよいことを示す(※使いどころを選ぶので上級者向け)。

そして、実務で一番よく使う(そして最も恩恵が大きい)のが、これらを組み合わせたショートハンドだ。

  • `contain: layout paint;`
  • あるいは、全部乗せの `contain: strict;` (size, layout, paintすべてを含む。ただし要素のサイズが中身に依存しなくなるため、幅や高さを明示する必要がある)
  • もしくは、サイズ以外のレイアウト・ペイントを独立させる `contain: content;` (迷ったらまずはこれをつけることが多い)

—

実務でどう使う? ライブチャットや無限スクロールでの実践コード

百聞は一見に如かずだ。例えば、よくある「次々と新しいメッセージが追加されるチャットログ」や「大量のカードが並ぶダッシュボードのウィジェット」を想像してほしい。

新しい要素が追加されるたびに、リスト全体がレイアウト計算の対象になってスクロールがカクつく。これを `contain` で華麗に解決してみよう。

悪い例(全域巻き込み型)

こんにちは!
お疲れ様です!

良い例(`contain: content` による最適化)

個々のメッセージアイテム、またはスクロールするコンテナ自体に `contain` を付与し、ブラウザの計算スコープを限定する。

avatar

ブラウザのレンダリング負荷を華麗にスルーするぜ。

/ スクロールビューポートのコンテナ /
.chat-viewport {
height: 400px;
overflow-y: auto;
/ レイアウトとペイントの最適化をブラウザに約束する /
contain: layout paint;
}

/ 各メッセージカード:もし複雑なHTML構造を持っていても、
外部(リスト全体)へレイアウトの変更が伝播しないようにする /
.message-card {
display: flex;
gap: 12px;
padding: 8px;
/ 個々のカードを独立したレイアウト・ペイントの島にする /
contain: content;
}

このコードを適用すると、ブラウザのDevTools(Performanceタブなど)で検証した際、新しい `.message-card` がDOMに追加されても、「Layout Shift(レイアウトの再計算)」の範囲がそのカードの境界内に限定されるようになる。ページ全体のDOMツリーを上から下まで舐め回すような重い計算が走らなくなるわけだ。これぞプロの技だね。

—

シニアから現場へのアドバイス:注意すべき「副作用」

さて、ここまで聞くと「じゃあ、すべてのDOM要素に `contain: content` を貼っとけば無敵じゃん!」と思うかもしれない。だが、そこは我らが複雑怪奇なWebブラウザの世界。甘い罠がある。

`contain` を使うときは、以下のリスクを頭に入れておいてほしい。

1. はみ出し(Overflow)のクリッピングに注意
`paint` や `content` を指定すると、要素の境界からはみ出したコンテンツが強制的に切り捨てられる(clipされる)。例えば、ドロップダウンメニューのポップアップや、要素の外側に少しはみ出させるような影(box-shadowの一部など)が、バッサリ消えてしまって「あれ? 表示が崩れたぞ?」と頭を抱える原因になる。はみ出しが必要な要素には `paint` を含まない `contain: layout` に留めるなどの配慮が必要だ。

2. サイズがゼロになる罠
`contain: size` や `strict` を使うと、要素の大きさが子要素の中身から独立するため、高さや幅をCSSで明示的に指定していないと、要素がペチャンコ(高さ0)になって消滅する。基本的には、高さや幅が固定されているウィジェットやグリッドアイテム、モーダル、あるいは `content`(sizeを含まない)を使うのが実務では安全で現実的だ。

—

まとめ:ブラウザと対話するフロントエンドへ

パフォーマンスチューニングというと、ついつい「JavaScriptをいかに速く動かすか」「不要なバンドルサイズを削るか」に目が向きがちだ。もちろんそれも大事。だけど、ブラウザという偉大なレンダリングエンジンの「クセ」を理解し、手助けしてやるアプローチも同じくらい、いや、それ以上に効果的だったりする。

`contain` プロパティは、まさにブラウザに対する「ここから先は君の仕事範囲外だから、安心してサボって(最適化して)いいよ」という優しいパスポートだ。

まずは今開発しているプロジェクトの中で、「ここ、独立したウィジェットなのに、動かすたびに全体が再描画されて重いな」と感じるパーツがあったら、そっと `contain: content;` を当ててみてほしい。DevToolsのRenderingパネルで「Paint flashing(ペイントの点滅)」を眺めてみれば、その効果に思わずニヤリとすること請け合いだ。

それじゃあ、今日も快適なブラウザライフを!何かハマったらまた相談してくれよ。

コメント

タイトルとURLをコピーしました