やあ。今日も今日とてパフォーマンスチューニングの沼にハマっているかい?
フロントエンドの現場にいると、「なんか最近、モーダルを開くたびに微小なカクつきがあるんだよな……」「無限スクロールの下の方でスクロールが重い……」なんて相談を、デザイナーやQAチームから受けることが本当によくあるよね。
で、大抵そういうときって、Chromeのパフォーマンスパネルを開くと、メインスレッドが真っ赤に染まっていて、レイアウト(Reflow)とペイントの嵐が起きていたりする。
「よし、じゃあ不要なCSSセレクタを削って、JSの再計算を抑えて……」って、ちょっと待った。
実は、現代のブラウザレンダリングの仕組みを根本からハックして、「ブラウザの無駄な苦労を強制的にシャットアウトする」ための強力な切り札があるんだ。それが今回解説する CSS Containment(`contain`プロパティ) だ。
公式仕様の解説を読むと眠くなるような堅苦しい言葉が並んでいるけれど、現場のシニアとして、この機能がブラウザの裏側でどう動き、どうやって君のコードを救うのか、生々しい仕組みとともに叩き込んでいこう。
—
1. ブラウザはなぜ遅くなるのか?(おさらいと現実)
まず、ブラウザが画面を描画するフロー(Critical Rendering Path)を思い出してほしい。
HTMLがパースされてDOMツリーができ、CSSが当たってCSSOMができ、それが合体してRender Treeが構築される。ここまではいい。
問題は、その後のLayout(レイアウト/リフロー)とPaint(ペイント)のフェーズだ。
Webページというのは、基本的に「全体がひとつながりの巨大なキャンバス」として扱われる。だから、DOMのどこか一箇所(例えば、画面の最下部にある小さなバッジの幅)が1px変わっただけでも、ブラウザのレンダリングエンジンは「他の要素に影響があるかもしれない」と怯え、原則としてドキュメント全体(あるいは大きなツリーの範囲)を走査してレイアウトを再計算しようとする。
……バカ正直すぎないかい?
画面の右端にある独立したウィジェットの中身が変わっただけなのに、なぜ左上のヘッダーや、全く関係ないサイドバーの位置まで再計算に巻き込まれなきゃいけないんだ。
この「無駄な全体波及(Global Cascade)」を防ぐために生まれたのが、`contain`プロパティなんだよ。
—
2. `contain`プロパティの正体:ブラウザへの「防壁」の張り方
`contain`は、一言で言うと「この要素の中の世界で起きたことは、外の世界には一切影響を与えないし、逆に外の状況も中は気にしなくていい」という契約を、ブラウザのエンジン(BlinkやGeckoなど)に結ばせる宣言だ。
このプロパティにはいくつかの値があって、それぞれの組み合わせで「何を隔離するか」を細かく制御できる。実務でよく使う主要な値を整理しておこう。
- `layout`: この要素の内部レイアウトは、外部の要素に絶対に関与しない(影響を与えない)。外部のレイアウト変更も、この要素内部には波及しない。
- `paint`: この要素の内部で描画されるものは、要素の境界からはみ出さない。もしはみ出してもクリップされるし、何よりこの要素自体が「一つの独立したペイント単位(レイヤー)」として扱われる。これにより、スクロール時などにこの中だけを独立して再描画できるようになる。
- `size`: この要素のサイズを計算する際、内部の子要素のサイズを一切見ない。つまり、子要素の高さが変わっても、親要素のサイズはビクともしない(固定サイズであることが前提)。
- `style`: プロパティのスコープ化(CounterやQuotesなど、カウンタの伝播範囲を制限する)。実務ではあまり直接意識することは少ないかもしれない。
そして、これらを組み合わせた便利なショートハンドがある。
- `contain: layout paint;`(または `strict` や `content`)
- `content`: `layout paint style` が含まれる。サイズは可変のままで、レイアウトとペイントを隔離したいときに一番よく使う。
- `strict`: `size layout paint style` すべてが含まれる。要素のサイズが中身に依存しない場合に最強のパフォーマンスを発揮する。
—
3. ブラウザの裏側:どうやってレンダリングコストを削っているのか?
ブラウザのレンダリングエンジンの視点に立ってみよう。
通常、JSでDOMを操作したり、CSSのアニメーションが走ったりすると、Layoutフェーズでは「どの要素がどこに配置されるか」をツリーの根から再評価する。
しかし、親要素に `contain: content;`(あるいは `layout paint`)が指定されていると、ブラウザのレイアウトエンジンはこう判断する。
> 「おっ、この要素の中身(子孫要素)は、外部のレイアウトツリーから完全に切り離されているな。じゃあ、この要素の中でいくらDOMが書き換わっても、この要素のバウンディングボックス(外枠のサイズ)が変わらない限り、親や兄弟要素のレイアウト計算を完全にスキップ(Pruning)してやろう!」
これが、レンダリングコスト激減のメカニズムだ。
特に、無限スクロールのリストアイテム、大量のカードが並ぶグリッド、独立したチャットのメッセージログなどでは、これが劇的な効果を生む。
—
4. 【実践】コピペで使える!パフォーマンス改善のサンプルコード
理屈はこれくらいにして、実際の現場でどう使うかコードを見ていこう。
今回は、「大量のアイテムが並び、頻繁に動的な更新やスクロールが発生するダッシュボードのカードウィジェット」を想定する。
HTML & CSS
このコードをブラウザで動かしてみるとわかるが、ウィジェットAの中で高頻度(0.3秒おき)にDOMが追加・削除され、スクロールが発生していても、隣にあるウィジェットBのレンダリングには一切悪影響が及ばない。
もし `contain: layout paint;` を外してみると、スペックの低いPCやモバイル端末では、ページ全体のレイアウトスラッシング(レイアウトの連鎖)を引き起こし、スクロールやアニメーションがカクつく原因になるんだ。
—
5. 現場で使う上での注意点(罠とハマりどころ)
さて、ここまで聞くと「じゃあ、すべてのカードやコンポーネントに `contain: content;` を貼りまくれば最強じゃん!」と思うかもしれない。
だが、そこは百戦錬磨のフロントエンドエンジニア。甘い罠には注意が必要だ。
1. `position: absolute` や `fixed` の基準が変わる
- `contain: layout` や `paint` を指定した要素は、「包含ブロック(Containing Block)」を形成する。
- つまり、その子孫要素に `position: absolute;` を持った要素がいる場合、これまでは画面全体や一番近い祖先を基準にしていたのが、「その `contain` が指定された要素の境界」が絶対配置の基準になってしまう。ポップアップやツールチップなどで「あれ、位置がおかしいぞ?」となったら大体これが原因だ。
2. はみ出し表現(Overflow)のクリッピング
- `contain: paint` を指定すると、要素の境界からはみ出たコンテンツ(例えば、少しはみ出させて立体感を出すドロップシャドウや、絶対配置のアイコンなど)が強制的に切り落とされ(クリップされ)てしまう。デザイン要件と衝突していないか必ず確認しよう。
3. サイズが変わらないことの担保 (`strict` や `size`)
- `contain: size` は「中身のサイズを親が計算に含めない」ため、親要素に明示的な `width` や `height`(あるいはCSS Grid/Flexの制約)が与えられていないと、中身が潰れて高さが 0 になってしまう。ここを勘違いして「レイアウトが崩れた!」とパニックになるジュニアエンジニアを何人も見てきた。安易な `strict` の乱用は禁物だ。
—
まとめ:ブラウザへの「配慮」がプロの腕の見せ所
Webフロントエンドのパフォーマンスチューニングというと、メモ化(`useMemo` / `React.memo`)や、JavaScriptのアルゴリズム最適化ばかりに目が行きがちだ。
しかし、ブラウザという「レンダリングの怪物」をいかに手なづけるか、そのCSSレイヤーでのコントロールを知っているかどうかが、シニアとジュニアを分ける大きな境界線になる。
「このコンポーネントは独立しているべきだ」という設計思想を、コード(JSやCSSの構造)だけでなく、`contain`プロパティという形でブラウザのエンジンに直接伝える。この一手間を惜しまないことが、ユーザーに極上の滑らかな体験を届けるプロの仕事というものさ。
さあ、今日のデプロイが終わったら、自社プロダクトの重いリストコンポーネントに `contain: layout paint;` をそっと仕込んで、パフォーマンス計測ツールを覗いてみなよ。きっと数字の変化にニヤリとできるはずさ。

コメント