パフォーマンス低下の3レイヤー
レスポンシブサイトの速度低下は「画像の転送量が多い」「CSSの解析に時間がかかる」「描画処理が重い」の3層で起きます。どれか1つを直せばよいのではなく、それぞれが連動しています。
全体像と優先順位
実際には「9割が画像の問題」というサイトも珍しくありません。最初に計測して、どの層のインパクトが一番大きいかを確認してから着手するのが最も効率的です。
最優先すべき順序を整理すると、次のようになります。
- 計測(計測ファースト)
- まずブラウザの開発ツールやLighthouseで現状を数値化します
- 画像の最適化
- 転送量削減の効果が最も大きいケースが多いため優先します
- CSS肥大化の解消
- @mediaの重複や不要ルールを整理して解析コストを下げます
- 描画処理の改善
- アニメーションやレイアウトシフトを引き起こすプロパティを確認します
- 再計測
- 改善後に数値を確認して効果を検証します
「このコードが重いはずだ」という思い込みで作業を進めると、実際にはほとんど効果のない最適化に時間をかけることになります。必ず数値でインパクトの大きい部分から始めましょう。
画像最適化の実装
CSSだけで縮小表示しても画像転送コストは下がらないため、HTML側で画像指定を変える必要があります。srcset / sizes / picture / loading="lazy" を組み合わせた現代的な実装を確認しましょう。
picture要素でフォーマットを切り替える
WebP形式は同品質のJPEGより30〜40%ほど小さいケースが多く、サポートされているブラウザに優先して配信できます。<picture> 要素を使うと、ブラウザのサポート状況に応じたフォールバックを記述できます。
<picture>
<source
type="image/webp"
srcset="hero-600w.webp 600w, hero-1200w.webp 1200w"
sizes="(max-width: 600px) 100vw, 80vw">
<img
src="hero-1200w.jpg"
srcset="hero-600w.jpg 600w, hero-1200w.jpg 1200w"
sizes="(max-width: 600px) 100vw, 80vw"
alt="ヒーロー画像"
loading="lazy"
width="1200"
height="675">
</picture> <source> で type="image/webp" を指定すると、WebP対応ブラウザはそちらを、非対応ブラウザは <img> の srcset を使います。
loading=“lazy” と width/height 属性
loading="lazy" をつけると、画面外にある画像はビューポートに近づいてから読み込まれます。ファーストビュー外の画像には積極的に活用します。
width と height 属性は、画像が読み込まれる前からブラウザが適切なスペースを確保するために重要です。これがないと、画像ロード後にページ全体がずれる「レイアウトシフト(CLS)」が発生します。
実際のプロジェクトでは srcset のURLを実際の画像パスに替えてください。ビルドツール(Vite、AstroのImage機能など)を使うと最適化後の画像を自動生成できます。
CSS最適化の実装
画像以外で改善できるのが、CSS側の肥大化と描画コストです。ここではCSS変数による整理と、content-visibility を使ったレンダリング最適化を扱います。
CSS変数で@mediaを1か所に集約する
@media の条件が各セレクターにバラバラに書かれていると、数値の変更時に全体を検索して直す必要があります。レスポンシブ対応値をCSS変数に集約し、変数の切り替えを @media の1か所だけで行う設計にすると、管理コストが大きく下がります。
:root {
--font-heading: clamp(1.4rem, 4vw, 2.2rem);
--font-body: clamp(0.9rem, 2vw, 1rem);
--space-section: clamp(24px, 5vw, 48px);
--grid-cols: 1;
--card-radius: 12px;
}
@media (min-width: 640px) {
:root {
--grid-cols: 3;
}
}
.lp-page {
font-family: sans-serif;
max-width: 900px;
margin: 0 auto;
padding: var(--space-section);
background: #f8fafc;
}
.page-header {
text-align: center;
margin-bottom: var(--space-section);
}
.headline {
font-size: var(--font-heading);
color: #1e293b;
margin: 0 0 8px;
}
.subline {
font-size: var(--font-body);
color: #64748b;
margin: 0;
}
.feature-section {
display: grid;
grid-template-columns: repeat(var(--grid-cols), 1fr);
gap: 16px;
}
.feature-card {
background: #fff;
border-radius: var(--card-radius);
padding: 24px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.feature-card i {
font-size: 2rem;
color: #10b981;
margin-bottom: 12px;
display: block;
}
.feature-card h2 {
font-size: 1rem;
margin: 0 0 8px;
color: #1e293b;
}
.feature-card p {
font-size: 0.85rem;
color: #64748b;
margin: 0;
} clamp() を使うと最小値・推奨値・最大値の範囲で自動的にサイズが変わるため、ブレークポイントの数を減らしながら滑らかな対応が実現できます。
プレビューの幅を変えると、640px境界でグリッドが1列から3列に切り替わることが確認できます。
描画処理の負荷を下げる
描画処理の重さは、ざっくり「レイアウト計算」「再描画(ペイント)」「合成(コンポジット)」の3段階で決まります。特に top / left / width などをアニメーションで動かすと、レイアウト計算が繰り返し発生してカクつきやすくなります。
レスポンシブサイトでは、スクロール中やボタンホバー時の小さな動きでも負荷が積み上がるため、動きは transform / opacity を中心に設計するのが基本です。
/* 負荷が高くなりやすい例: レイアウトを毎フレーム再計算 */
.cta-button-bad {
position: relative;
left: 0;
transition: left 0.25s ease;
}
.cta-button-bad:hover {
left: 8px;
}
/* 改善例: コンポジット中心で処理 */
.cta-button-good {
transition: transform 0.25s ease, opacity 0.25s ease;
}
.cta-button-good:hover {
transform: translateX(8px);
opacity: 0.92;
} この考え方を先に押さえておくと、後半の誤用パターン(top/leftアニメーション)も「なぜ避けるべきか」が理解しやすくなります。
content-visibility で描画外の要素をスキップする
ページが長くなると、スクロールしていない部分のレイアウト計算が初期表示の速度を下げることがあります。content-visibility: auto をセクションに設定すると、ビューポート外のレンダリング計算を必要なタイミングまで遅延できます。
/* 長いランディングページの各セクションに適用 */
.lp-section {
content-visibility: auto;
/* ジャンプ防止のためおおよその高さを指定する */
contain-intrinsic-size: 0 600px;
} contain-intrinsic-size を省くと、スクロールバーの高さが突然変わることがあります。おおよその高さを設定しておくと違和感が出ません。
運用規模別の優先順位の決め方
同じ最適化でも、サイトの運用形態によって優先順位は変わります。ここを先に決めると、無駄な作業を減らせます。
- 小規模の静的サイト(個人制作・ページ数が少ない)
- 画像最適化を最優先にし、CSSは重複削減までで十分なことが多いです
- CMS主体サイト(記事ページが大量に増える)
- 画像の自動変換・自動リサイズの仕組みを先に作り、運用時の手作業をなくします
- チーム開発案件(複数人で継続改修)
@media条件値の統一ルール、レビュー観点、計測手順をドキュメント化して再発を防ぎます
誤用パターンと代替策
全体像と実装の基礎を押さえたうえで、最後に見落としやすい誤用を4つ確認します。
誤用1:CSSリサイズだけで元画像は大きいまま
max-width: 100% でCSSによる縮小表示はできますが、ブラウザが実際にダウンロードするファイルサイズはそのままです。スマホが2MB超の画像を受け取ることになります。
<!-- 元画像が2000×1200px / 2.1MB でもCSSで縮小するだけでは全デバイスが2.1MBを受け取る -->
<img src="hero.jpg" alt="ヒーロー画像"> .hero img {
max-width: 100%;
height: auto;
} 代わりに srcset と sizes を使って画像自体を複数用意し、画面幅に応じたファイルを配信します。
<img
src="hero-600w.jpg"
srcset="hero-600w.jpg 600w, hero-1200w.jpg 1200w, hero-2000w.jpg 2000w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px"
alt="ヒーロー画像"
loading="lazy"
width="1200"
height="675"> 誤用2:display:none による非表示の落とし穴
CSSで display: none をつけても、img の src 属性がHTMLに存在すれば多くのブラウザは画像をダウンロードします。DOMにノードが残っている限り、HTMLの解析コストも発生します。
/* スマホから見えないようにしたが、画像は読み込まれてしまう */
.desktop-only {
display: none;
}
@media (min-width: 768px) {
.desktop-only {
display: block;
}
} モバイルで不要なコンテンツは、基本的にHTML自体を出力しない方針に切り替えます。display: none や hidden で隠すだけでは、読み込みコストが残るケースがあるためです。
誤用3:@mediaが各セレクターにバラバラに記述される
同じ条件値の @media がファイル内に散在すると、数値を変更するたびに全箇所を検索して直す必要が出ます。特に複数人で開発するプロジェクトでは、誤って同じ @media を二重に書いてしまうケースが増えやすく、デバッグに時間がかかります。
.hero { font-size: 2rem; padding: 32px; }
.card { margin-bottom: 24px; }
@media (max-width: 768px) { .hero { font-size: 1.4rem; padding: 20px; } }
@media (max-width: 768px) { .card { margin-bottom: 16px; } }
/* 同じ条件が各所に散らばり、変更時に漏れが発生しやすい */ 誤用4:アニメーションにtop/leftを使う
top や left などの位置プロパティを変化させるアニメーションは、ブラウザがレイアウト再計算を毎フレーム行います。transform: translate() に置き換えることでGPUを使ったコンポジットアニメーションになり、CPU負荷を大幅に抑えられます。
/* NG:topを変えるとレイアウト再計算が毎フレーム発生する */
.item-ng {
position: absolute;
top: 0;
transition: top 0.3s ease;
}
.item-ng:hover {
top: -8px;
}
/* OK:transformはGPUコンポジット層で処理される */
.item-ok {
position: relative;
transition: transform 0.3s ease;
}
.item-ok:hover {
transform: translateY(-8px);
} will-change: transform は、アニメーションが始まる直前に設定し、終わったら外すのが理想です。全要素に常時設定するとGPUメモリを常に消費し、逆効果になることがあります。
理解度チェッククイズ
パフォーマンス最適化クイズ
まとめ
- 計測ファースト
- Lighthouseや開発ツールで現状の数値を確認してから行動します
- srcset + sizes + loading=“lazy”
- 画像最適化はまずこの3つから始めます
- width / height 属性でCLSを防ぐ
- 画像領域を事前に確保することで読み込み後のレイアウトシフトをなくします
- CSS変数で@media一元管理
- 条件の値は直書き、プロパティの値をvar()で受け取る設計にします
- アニメーションはtransform / opacity
- top / leftを動かすとレイアウト再計算が毎フレーム発生します