CSSで表示を速くする — セレクタの最適化は、条件を揃えて測ったら0.1msだった

先に結論

「子孫セレクタは遅いから避ける」「IDセレクタが最速」という助言が、いまも検索結果の上位に並んでいます。 条件を揃えて測ったら、差は0.1ミリ秒でした。

表1:昔よく言われた助言は、いま効くか
助言いま
子孫セレクタを避ける効果はほぼ無い。測ったら0.1msの差(詳しく)
IDセレクタを使う速く見えるのは対象要素が少ないから(詳しく)
セレクタは右から左に解釈される事実だが、だから遅いという結論にはならない
CSS3の使用は最小限にいまは前提が違う。使って問題ない
CSSの読み込みがレンダリングを止めるこれはいまも効く。本命(詳しく)

手を付ける順番は、こうなります。

表2:効く順
順やること効くもの
1CSSの読み込みがレンダリングを止める時間を減らすLCP・FCP
2レイアウトのずれを無くす(画像に寸法を書く等)CLS
3画面外の描画を遅らせる(content-visibility)長いページの初期表示
4再フローを減らす(読み書きの順番)操作の反応(INP)。945倍の差が出た
最後セレクタを簡潔にする保守性。速度はほぼ変わらない

セレクタの最適化を測ってみた

条件を揃えて比べる

セレクタの速度を比べるとき、うっかりすると「何要素にマッチするか」を比べてしまいます。 IDは1要素、クラスは数千要素、では比較になりません。

そこで、すべて同じ2,000要素にマッチするセレクタを用意しました。 DOM全体は12,008要素です。スタイルを差し替えて、再計算が終わるまでの時間を51回測り、中央値を取りました。

表3:同じ2,000要素にマッチするセレクタの比較(Chrome 152で実測)
セレクタマッチ数中央値
.t2,0001.8ms
span.t2,0001.8ms
li .t2,0001.8ms
.wrap section article ul li span.t(4段)2,0001.9ms
[id^="t"](属性の前方一致)2,0001.9ms
:not(.zzz).t2,0001.8ms

最速と最遅の差は 0.1ミリ秒でした。 12,008要素のDOMで、2,000要素を一斉に塗り直して、この差です。 「子孫セレクタ4段を避ける」ことで得られるのは、この0.1msです。

「IDが最速」の正体

条件を揃えない比較もやってみました。こちらだと、たしかにIDが速く見えます。

表4:マッチ数を揃えなかった場合(実測)
セレクタマッチ数中央値
#t199910.7ms
.t2,0001.8ms
4段の子孫セレクタ2,0002.0ms

0.7msと1.8msの差は、セレクタの複雑さではありません。 1要素を塗り直すか、2,000要素を塗り直すかの差です。

「IDセレクタが最速」という助言は、この2つを混同しています。 同じ数の要素にスタイルを当てるなら、表3のとおり、 書き方を変えてもほとんど変わりません。

MDNは何と言っているか

公平に書いておくと、MDNもセレクタの簡略化は勧めています。 ただし、理由の並べ方を見てください。

「セレクターを簡略化することが重要である。複雑なセレクターは、パフォーマンスに悪影響を与え、 メンテナンスも難しくなる」
— CSS のパフォーマンス最適化(MDN・要約)

そしてMDNは、CSSの主な課題として別のものを挙げています。

「CSSが引き起こす主なパフォーマンス課題としては、レンダリングブロックと再フローの最小化が挙げられる」
— 同上

つまり「セレクタを簡潔に書く」は正しい助言です。 ただし、速度のためではなく保守のためと考えたほうが実態に合います。 速度で効くのは、MDNが先に挙げている2つです。

測る指標が変わった

いまの3つ

2010年代前半は「表示が速い/遅い」を体感で語っていました。 いまは測り方が決まっています。

表5:Core Web Vitals(web.devの記載)
指標意味「良好」の基準
LCP(Largest Contentful Paint)一番大きい要素が描画されるまで2.5秒
INP(Interaction to Next Paint)操作してから次の描画まで200ミリ秒
CLS(Cumulative Layout Shift)レイアウトのずれの量0.1

判定は75パーセンタイルで行われます。 平均ではありません。遅いほうの4人に1人がどうかを見ます。 「自分の環境では速い」が通用しないのはこのためです。

FIDは廃止された

検索結果にはいまも FID を挙げている解説がありますが、これは置き換わっています。

「2022年5月に試験運用版の指標として Interaction to Next Paint(INP)を導入しました。 昨年、INPは保留中の指標となり、2024年3月にINPが安定ステータスに昇格することを発表しました。 本日、INPが公式にCore Web Vitalsとして3月12日にFIDに代わり、この移行に伴いFIDが廃止されることを……」
— 3月12日に「Interaction to Next Paint」がウェブに関する主な指標に(web.dev・要約)

FIDと書いてある解説は、2024年3月より前の情報です。 その記事の他の内容も、同じだけ古い可能性があります。

CSSで本当に効くのはレンダリングブロック

なぜ止まるのか

CSSは、読み終わるまで描画を止めます。 スタイルが分からないうちに描くと、あとから見た目が変わってしまうからです。

「CSSは、ブラウザーがCSSを要求していると判断するまでレンダリングをブロックする」
— CSS のパフォーマンス最適化(MDN・要約)

だから、CSSのサイズと本数がそのまま最初の描画の遅さになります。 セレクタの書き方は、そのあとの話です。

このサイトの記事ページで測った値を載せておきます。

表6:このサイトの記事ページ(実測)
項目値
DOM要素数642
同一オリジンのリクエスト数2(HTML+CSS)
CSSの本数・サイズ1本・15,663バイト
FCP(最初の描画)176ms
LCP176ms
LCPの要素h1

FCPとLCPが同じ値になっています。 画像が無く、一番大きい要素が h1 のテキストなので、 最初の描画がそのまま最大要素の描画になるからです。

この数字はローカルサーバーでの測定なので、絶対値は参考になりません。 意味があるのは関係のほうです。止めているものがCSS1本しかないページでは、 LCPを縮める余地もCSSの読み込みにしかありません。

メディアクエリで非ブロッキングにする

MDNが挙げている手が、これです。

「メディアクエリーを使用して非ブロッキングのCSSを作成し、特定の条件にのみ適用されるようにすることで、 レンダリングブロックを最小限に抑えることができる」
— 同上

<!-- 常に要るものだけをブロッキングにする -->
<link rel="stylesheet" href="/base.css">

<!-- 印刷用・広い画面用は、条件が合うときだけブロックする -->
<link rel="stylesheet" href="/print.css" media="print">
<link rel="stylesheet" href="/wide.css" media="(min-width: 1200px)">
表7:レンダリングブロックを減らす手
手効果注意
CSSを1本にまとめる往復が減るサイズが大きくなりすぎると逆効果
media で条件を付ける条件外ではブロックしないMDNが挙げている手
最初の画面ぶんをインラインにする往復が0になるHTMLが毎回大きくなる。やりすぎない
使っていないCSSを消すサイズが減る消しすぎると壊れる。計測しながら
@import をやめる直列の往復が消えるCSSの中からさらにCSSを呼ぶ形はいまも遅い

最後の行は、昔から変わらず有効です。 @import は、1本目を読み終わってから2本目を取りに行くので、待ち時間が足し算になります。 <link> を並べれば同時に取りに行けます。

もう一方の課題、再フロー

MDNが挙げるもう一方が再フローです。 こちらは、セレクタとは桁が違いました。

2,000個の要素に対して、①高さを読んで、すぐ高さを書くを交互に繰り返す場合と、 ②全部読み終えてから、全部書く場合を測りました。やっている仕事は同じです。

表8:読み書きの順番を変えただけの差(2,000要素・Chrome 152で実測)
やり方中央値
読む→書く を交互に繰り返す377.9ms
全部読む→全部書く0.4ms
差約945倍

セレクタの書き換えで取れるのが0.1ms、順番を直すと378msです。 どちらに時間を使うべきかは、この2つを並べれば決まります。

なぜこうなるか。offsetHeight のような値を読もうとすると、 ブラウザは「いま正しい値」を返すために、保留していたレイアウト計算を強制的に終わらせます。 書いた直後に読めば、また計算させることになります。2,000回繰り返せば、2,000回計算させます。

// 遅い:読む→書く を交互に(2,000要素で 378ms)
for (const el of els) {
  el.style.height = el.offsetHeight + 1 + 'px';   // 読んだ直後に書いている
}

// 速い:読み切ってから、書き切る(同じ処理で 0.4ms)
const hs = els.map(el => el.offsetHeight);        // まず全部読む
els.forEach((el, i) => { el.style.height = hs[i] + 1 + 'px'; });

レイアウトを強制的に計算させるプロパティを覚えておくと、見分けがつきます。

表9:読むとレイアウト計算が走るもの(代表例)
種類例
寸法offsetWidth / offsetHeight / clientWidth / clientHeight
位置offsetTop / offsetLeft / getBoundingClientRect()
スクロールscrollTop / scrollHeight
算出値getComputedStyle() の寸法系プロパティ

ループの中にこれらが出てきたら、そこを見てください。 CSSの書き方の問題ではなく、CSSを読み書きする順番の問題です。 これはセレクタをいくら簡潔にしても直りません。

いまのCSSにある道具

2012年には無かったもので、速度に直接効くプロパティがあります。 MDNも挙げています。

表10:描画を減らすプロパティ
プロパティ何をするか使いどころ
content-visibilityブラウザがレイアウトとレンダリングを遅らせられる画面外の長いコンテンツ
will-change変更が予想される要素をブラウザに知らせるアニメーションの直前。常時付けない
contain影響範囲をその要素に閉じ込める独立した部品
/* 画面外の記事カードは、入ってくるまで描かない */
.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 300px;   /* 想定の高さを伝える */
}

contain-intrinsic-size を一緒に書いてください。 高さを伝えないと、スクロールバーの長さが途中で変わり、 レイアウトのずれ(CLS)になります。速くするつもりで別の指標を悪くします。

will-change は常時付けると逆効果です。 ブラウザに「これから変わる」と予告して準備させるものなので、 ずっと予告していると、ずっと資源を抱えたままになります。

どこから手を付けるか

推測で直さないでください。 どこが遅いかは、ページによって違います。

表11:症状から手を付ける場所
症状疑う場所
最初の描画まで真っ白CSSの読み込み(詳しく)。サーバーの応答も見る
文字は出るが画像が遅いLCPの要素が画像。寸法指定・形式・優先度
表示後にガタつくCLS。画像・広告・Webフォントの読み込み
スクロールが重い長いDOM。content-visibility を検討
クリックの反応が鈍いINP。読み書きの順番を疑う(詳しく)
なんとなく重いまず測る。セレクタを疑うのは最後

最後の行が、この記事の主題です。 「なんとなく重い」からセレクタを書き直すのは、0.1msを取りに行く作業です(表3)。 先に測って、どこで時間を使っているかを見てください。

表12:測る手段
手段分かること
開発者ツールの Network タブ何がどれだけ描画を止めているか
開発者ツールの Performance タブスタイル計算・レイアウト・描画の内訳
performance.getEntriesByType('paint')FCPの時刻
PerformanceObserver(largest-contentful-paint)LCPの時刻と、その要素
実際の利用者の数値75パーセンタイルで判定される(詳しく)

4行目が効きます。LCPの「要素」が取れるので、 何を速くすればいいかが一発で決まります。 画像ならその画像、見出しならCSSの読み込み、という具合です。

AIに任せるときの注意

表13:AIへの頼み方
頼むこと向くか理由
Networkタブの一覧を貼って「何が描画を止めている?」向くブロッキングの判定は機械的
「media 付きに分割する案を出して」向く条件の切り出しは読めば分かる
「content-visibility を付けるならどこ?」向く画面外になりやすい部品は構造から判断できる
「CSSを高速化して」向かないセレクタの書き換えが返る。0.1msの作業
「Core Web Vitalsを説明して」向かないFIDが出てきたら2024年3月より前の情報
実際の数値の判定向かない75パーセンタイルの実測値は自分で見る

判定の目安です。答えに「子孫セレクタを避ける」「IDセレクタを使う」が出てきたら、2010年代前半の情報です。 レンダリングブロックの話が出てくれば、現代的です。

よくある質問

子孫セレクタは避けるべきですか

速度のためなら、避ける必要はありません。 条件を揃えて測ったところ、4段の子孫セレクタと単純なクラスセレクタの差は0.1ミリ秒でした(表3)。 保守のために簡潔に書くのは、別の話として正しいです。

IDセレクタが一番速いのでは

比較の条件が揃っていません。 IDは1要素、クラスは数千要素にマッチします。 差の正体は「何要素を塗り直すか」です(表4)。

セレクタが右から左に解釈されるのは本当ですか

解釈の仕組みとしてはそのとおりです。 ただし「だから遅い」という結論は、実測では出ませんでした(表3)。

では何をすればいいですか

CSSの読み込みが描画を止めている時間を減らすことです(詳しく)。 MDNもCSSの主な課題として「レンダリングブロックと再フローの最小化」を挙げています。

FIDはどうなりましたか

2024年3月12日にINPへ置き換わり、廃止されました(web.dev)。 FIDを挙げている解説は、それより前の情報です。

content-visibility を付ければ速くなりますか

画面外の長いコンテンツには効きます。ただし contain-intrinsic-size で想定の高さを伝えてください(詳しく)。 伝えないとレイアウトのずれ(CLS)になります。

再フローとは何ですか

レイアウトの再計算です。寸法や位置を読むと、ブラウザは正しい値を返すために計算を終わらせます。 読んだ直後に書くのを繰り返すと、その回数だけ計算が走ります。 2,000要素で測ったら378ms、順番を直すと0.4msでした(表8)。

自分の環境では速いのですが

判定は75パーセンタイルで行われます(詳しく)。 平均でも最速でもなく、遅いほうの4人に1人がどうかで決まります。

@import はいまでも遅いですか

遅いです。1本目を読み終わってから2本目を取りに行くので、待ち時間が足し算になります(表7)。 <link> を並べれば同時に取得できます。

まとめ

サーバーの応答が遅いのではと思ったら レンタルサーバーおすすめ8選、 リダイレクトで往復が増える話は wwwあり・なしを統一する、 CSSだけでできることは URLのアンカーで処理を分ける にも書きました。

セレクタの測定値は、2026年10月25日に Chrome 152.0.7977.167 で、 12,008要素のDOMに対して51回測った中央値です。 再フローの測定(378ms / 0.4ms)は同じ日・同じブラウザで2,000要素に対して11回測った中央値です。 DOMの規模・構造・ブラウザが変われば結果は変わります。 このサイトの数値はローカルサーバーでの測定なので、絶対値は参考になりません。 自分のページで測ってから手を入れてください。