CSSのcontent-visibilityで長いページを速く描画する

​​CSS​JavaScript,Playwright

ブログ記事や商品一覧のような長いページでは、CSS の content-visibility: auto を section のような、まとまりごとの要素に付けると、画面から離れた部分のレイアウトと描画をブラウザーが後回しにします。高さの見積もりを contain-intrinsic-size で一緒に書くのが、効果を出すための要点です。

css
section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}
補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153(CPU 4コア)です。時間は、この環境で測った値です。Firefox と Safari、スマートフォンの実機、ブラウザーのページ内検索(Ctrl+F)の画面操作、スクリーンリーダーでの読み上げは試していません。

section 200個のページで、最初の描画までの時間が約7分の1になった

見出し、段落12個、10行の表を入れた section を200個並べたページを作り、ページの HTML を document.write() で書き込んでから、最初のフレームが描かれるまでの時間を、7回ずつ測りました。1つの section の高さは約859px、ページ全体の高さは 171,747px です。

3本の横棒のグラフ。指定なしは 206.0 ms、content-visibility: auto だけは 60.8 ms、content-visibility: auto と contain-intrinsic-size: auto 800px の組み合わせは 30.5 ms
section 200個のページで、最初の描画までの時間(7回の中央値)
section に付けた CSS 最初の描画まで うちレイアウト うちスタイル計算
指定なし 206.0ms 118.2ms 26.9ms
content-visibility: auto だけ 60.8ms 29.6ms 7.0ms
content-visibility: auto と contain-intrinsic-size: auto 800px 30.5ms 7.2ms 1.8ms

時間はどれも中央値です。レイアウトとスタイル計算の時間は、Chromium の DevTools Protocol(Performance.getMetrics)で数えた値です。両方を指定すると、最初の描画までの時間は指定なしの約7分の1になりました。

速くなるのは、画面から離れた section の中身を、ブラウザーが計算しないからです。CSS Containment Module Level 2 の仕様では、auto の要素が「ユーザーに関係がある」状態でないとき、中身のレイアウトと描画を省くと決めています。関係がある状態とは、要素が画面か、その周りの余白に入っているとき、中の要素にフォーカスがあるとき、中の文字が選択されているとき、などです。

開いた直後に中身を描画していた section は、両方を指定したページでは第1節と第2節だけでした。残りの198個は中身の計算を省いていました。どの section が省かれているかは、中の見出しに対して checkVisibility({ contentVisibilityAuto: true }) を呼ぶと調べられます。省かれているときは false が返ります。

contain-intrinsic-size で高さを見積もる

content-visibility: auto だけでも速くなりましたが、ページの高さが大きく変わりました。

section に付けた CSS 開いた直後のページの高さ 第100節の高さ
指定なし 171,747px 858.7px
content-visibility: auto だけ 44,584px 33px
content-visibility: auto と contain-intrinsic-size: auto 800px 166,651px 833px

中身の計算を省いた section は、中身がないものとして大きさが決まります。content-visibility: auto だけでは、第100節の高さは上下の padding と枠線の分の 33px になりました。ページの高さは本来の約4分の1です。

この状態で上から1画面ずつ一番下までスクロールすると、ページの高さは 15通りの値に変わりながら、44,584px から 171,747px まで伸びました。スクロールバーのつまみの大きさと位置が、読み進めるうちに変わり続けることになります。

contain-intrinsic-size: auto 800px を書くと、中身の計算を省いた section は、中身の高さを 800px として扱います。padding と枠線を足すと 833px です。開いた直後のページの高さは 166,651px で、本来の高さとの差は約3%でした。スクロールしたときも高さは変わりますが、1回の変化は、見積もりと実際の差(1つの section あたり約26px)の分だけです。見積もりの値は、実際の要素のおおよその高さに合わせます。

content-visibility: auto だけのページでは、開いた直後に 46個の section が、中身を計算したときと同じ高さ(859px)になっていました。残りの154個は 33px です。このとき中身の計算を省いていた section は198個なので、46個のほとんどは、一度中身を計算してから、また省く状態に戻っています。この46個が覚えた高さを使う動きは、次の節で説明します。

auto を付けると、一度表示した高さを覚える

contain-intrinsic-size の値の前に auto を付けると、一度中身を計算した要素は、その大きさを覚えます。中身の計算を省いているあいだは、見積もりの 800px ではなく、覚えた大きさが使われます。CSS Box Sizing Module Level 4 の仕様で決められています。

検証環境でも、一番下までスクロールしたあとは、200個の section がすべて 859px になり、ページの高さは本来の 171,747px と同じになりました。一番下の2つ以外の section は中身の計算を省いた状態に戻っていますが、高さは変わりません。

ポイント

検証環境の Chromium 153 では、contain-intrinsic-size を書かないときの計算値が auto none、800px とだけ書いたときの計算値が auto 800px でした。どちらも、スクロールしたあとは覚えた高さを使っていました。ただし仕様では、覚えた大きさを使うのは auto を書いたときだけです。仕様どおりに覚えた高さを使わせるには、auto 800px のように auto を書いておきます。

フォーカス、ページ内リンク、文字列の検索は使える

中身の計算を省いていても、section の中の要素は DOM に残っています。MDN の content-visibility のページによると、アクセシビリティツリーにも残ります。仕様では、省いた中身もページ内検索やタブ移動で普通に使え、フォーカスや選択もできなければならないと決めています。

検証環境では、両方を指定したページで、次の3つを試しました。どれも、対象の section は中身の計算を省いた状態から始めています。

  • 第150節のリンクに focus() を呼ぶと、ページがその位置までスクロールし、第150節の中身が描画されました。
  • location.hash を #s150 にすると、第150節の上端が画面の上端にぴったり合いました。
  • Chromium の window.find() で第180節にある文字列を探すと、見つかって選択され、ページがその位置までスクロールし、第180節の中身が描画されました。

window.find() は標準外の機能です。ここではページ内検索の代わりに使いました。

はみ出す子要素は切れる

content-visibility: auto を付けた要素には、画面の近くにあるときも、仕様の「描画の封じ込め(paint containment)」がかかります。そのため、子要素のうち、要素の枠の外へはみ出した部分は表示されません。

上下2枚のカードの画面。上は指定なしで、右上のピンクの吹き出しがカードの上の枠から上にはみ出して全部見えている。下は content-visibility: auto で、吹き出しの、カードの枠より上の部分が切れて、文字の下半分だけが見えている
上は指定なし、下は content-visibility: auto。右上の吹き出しは、カードの枠から上へ 20px はみ出す位置に置いている

吹き出しは position: absolute で、カードの枠より 20px 上に置いています。content-visibility: auto のカードでは、枠より上の部分が切れました。一方、カード自身に付けた影(box-shadow)は、どちらでも表示されています。

ドロップダウンメニューやツールチップのように、枠の外へ出す要素がある部品には、content-visibility: auto を付けないようにします。

画面に出入りしたときに処理を止める

中身の計算を省き始めたときと、やめたときには、要素に contentvisibilityautostatechange イベントが届きます。イベントの skipped が true なら省き始めたときです。canvas のアニメーションのように、画面にないあいだは止めたい処理の切り替えに使えます。

js
section.addEventListener('contentvisibilityautostatechange', (e) => {
  if (e.skipped) {
    // 画面から離れた:アニメーションなどを止める
  } else {
    // 画面に近づいた:再開する
  }
});

検証環境では、ページを開いた直後に、合わせて200件のイベントが届きました。skipped が false のものは第1節と第2節の2件で、残りの198件は true です。第10節の位置までスクロールすると、第1節と第2節に true、第8節から第11節に false が届きました。画面の高さは 600px なので、第10節だけで画面が埋まります。それでも、画面の外にある第8節、第9節、第11節の描画も始まりました。

MDN のイベントの説明では、画面にないあいだも中身は支援技術などから使われるので、このイベントを、内容に関わる DOM の更新を止める合図には使わないように書かれています。

ブラウザーの対応

MDN の互換性データ(browser-compat-data)では、content-visibility は Chrome 85、Firefox 125、Safari 18 から使えます。ただし、Safari 18 の auto は部分的な実装で、省いた中身がページ内検索で見つからないと注記されています。Safari 26 からは、部分的な実装という注記がなくなっています。

​Posted by 86