やあ、現場で戦うエンジニア諸君。今日もDOMの増殖やレイアウト・スラッシング(Layout Thrashing)と格闘しているかな?
ブラウザのレンダリング最適化といえば、かつては「DOM要素を減らせ」「`requestAnimationFrame`を使え」「`will-change`でGPUを叩き起こせ」といった手法が定石だった。だが、現代のモダンブラウザはさらに強力な武器を我々に授けてくれている。
それが今回解説する `content-visibility` だ。
「画面外の要素は描画しなきゃいいじゃないか」という、我々が長年夢にまで見た「究極の手抜き(最適化)」を、ブラウザエンジンレベルで実現するこのプロパティ。その裏側で何が起きているのか、そして実務でどう使いこなすべきか。チーフアーキテクトの視点から、泥臭い最適化の真髄を伝授しよう。
—
なぜ「見えない要素」が重いのか? ブラウザの苦悩
まず、ブラウザが画面を映し出すまでの「レンダリング・パイプライン」を思い出してほしい。
1. Style: CSSを解析して各要素に適用する。
2. Layout (Reflow): 各要素の大きさや位置を計算する。
3. Paint (Repaint): ピクセルを塗りつぶす。
4. Composite: 重なりを合成して画面に出力する。
問題は、たとえユーザーの視界(ビューポート)に入っていなくても、DOMツリーに存在する以上、ブラウザは律儀に Style と Layout、そして多くの場合 Paint の計算をバックグラウンドで律儀にやり続けてしまう点にある。
特に、数千件のカードが並ぶ無限スクロールや、巨大なドキュメント。これらを素直にレンダリングしようとすると、初期ロード時に数秒のメインスレッド占有が発生し、スクロールのたびにガタつき(Jank)が生じる。これが現場でよく見る「DOMの重みで死ぬ」現象だ。
救世主 `content-visibility: auto`
ここで登場するのが `content-visibility: auto` だ。
このプロパティを要素に付与すると、ブラウザはこう判断する。
「おっと、この要素は今画面外だな。じゃあ、レイアウトもペイントも、ついでにスタイル計算も全部スキップして、空っぽの箱として扱っておこう」
これがどれほど劇的か分かるだろうか? ブラウザの最も重い処理である「Layout」と「Paint」を、その要素が画面内に入ってくる直前まで完全に免除するんだ。JavaScriptで `IntersectionObserver` を自作して `display: none` を切り替えるような、あの涙ぐましい努力をブラウザがネイティブのC++層でやってくれる。
裏側で起きていること:レンダリングの「封じ込め」
`content-visibility: auto` を指定すると、内部的には `contain: strict` に近い制約がかかる。つまり、その要素のサイズや中身が外側のレイアウトに影響を与えないよう「隔離」されるんだ。
ブラウザは画面外にある間、その中身(子要素)を無視する。だが、ここで一つ大きな問題が発生する。「中身を無視したら、その要素自体の高さが0になってしまう」 という問題だ。
—
現場の落とし穴:スクロールバーが「暴れる」現象を回避せよ
何も考えずに `content-visibility: auto` を使うと、ページを開いた瞬間にスクロールバーがガタガタと動く、あるいはスクロールするたびにスクロール位置が飛ぶという、最悪のユーザー体験を招く。
なぜか? ブラウザが画面外の要素のレンダリングをスキップした結果、その要素の高さが一時的に「0」として計算されるからだ。スクロールしてその要素が近づき、いざレンダリングが始まると、突然正しい高さ(例えば500px)が算出される。するとページ全体の長さが急に伸び、スクロール位置が狂う。
これを防ぐのが `contain-intrinsic-size` プロパティだ。
.card-item {
/ これが魔法の呪文。画面外ならレンダリングをスキップする /
content-visibility: auto;
/
ブラウザに「中身を描画しなくても、だいたいこれくらいの高さがあるよ」と教える。
これがないと、スクロールバーがガタつく原因になる。
/
contain-intrinsic-size: 0 500px;
}
最近のブラウザなら、`contain-intrinsic-size: auto 500px;` のように `auto` をつけるのがベストプラクティスだ。一度レンダリングされた後の「実際の高さ」をブラウザが記憶してくれるようになり、精度の高いプレースホルダーとして機能する。
—
実戦投入:コピペで使える最適化コード例
では、実際にリストレンダリングを最適化するコードを見てみよう。大量のコンテンツがあるページを想定している。
爆速レンダリング・デモ
セクション 1
ここには重い画像や、複雑なDOM構造が入ると想定してくれ…
セクション 2
スクロールして近づくまで、この中身の計算は「予約」状態になる。
—
アーキテクトとしての忠告:使い所の見極め
最後に、この強力な武器を使う上での注意点を伝えておこう。
1. アクセシビリティ(A11y)は大丈夫か?:
`content-visibility: auto` で隠れているコンテンツは、実は「ブラウザ内検索(Ctrl+F)」にヒットするし、スクリーンリーダーからもアクセス可能だ(ここが `display: none` との決定的な違いだ)。ただし、古いブラウザや特定の支援技術での挙動は常に検証を怠らないこと。
2. 初期表示で見える位置には使わない:
ファーストビュー(Above the fold)にある要素にこれを適用すると、逆に「サイズ計算のオーバーヘッド」でわずかにレンダリングが遅れる可能性がある。あくまで「スクロールしないと見えない大量のコンテンツ」に適用するのが定石だ。
3. JavaScriptでの要素取得:
`content-visibility: auto` で隠れている要素に対して `getBoundingClientRect()` などを実行すると、その瞬間に強制的にレイアウト計算が走り、パフォーマンスのメリットが相殺される(Layout Thrashing)。JSでゴリゴリ触る要素には注意が必要だ。
まとめ
`content-visibility` は、我々エンジニアがこれまで JavaScript で苦労して実装してきた「仮想リスト(Virtual Scroll)」の概念を、CSS 1行でブラウザネイティブに持ち込んだ革命だ。
初期ロードの TBT (Total Blocking Time) を劇的に減らし、LCP (Largest Contentful Paint) の改善にも寄与する。もし君が今、重いページパフォーマンスに頭を抱えているなら、この「賢い手抜き」を試さない手はない。
ブラウザの仕組みを理解し、その力を正しく引き出す。それが一流のエンジニアの仕事だ。健闘を祈る!

コメント