レスポンシブ対応のCLS改善方法を解説!画像などコンテンツ別に紹介

レスポンシブ対応のCLS改善方法を解説!画像などコンテンツ別に紹介

レスポンシブサイトが原因のCLS(Cumulative Layout Shift)に悩んでいませんか?

画像の遅延読み込みやサイズ未指定により、ページ表示時にレイアウトが崩れ、ユーザー体験を損ねているケースは少なくありません。

本記事では、width・height属性の正しい指定方法、aspect-ratioの活用、srcset・sizes属性の実装など、CLSスコアを劇的に改善する実践的なレスポンシブ画像対応テクニックを、具体的なコード例とともに徹底解説します。

CLSとは?レスポンシブサイトで問題になる理由

CLS(Cumulative Layout Shift:累積レイアウトシフト)とは、ページ読み込み中に予期しないレイアウト移動が発生する度合いを数値化したWeb指標です。

Googleが重視するCore Web Vitalsの一つで、0.1以下が良好、0.25以上は改善が必要とされています。

レスポンシブサイトでは、デバイスごとに画面サイズが異なるため、CLSが特に問題となります。

画像やバナーのサイズを事前指定しないと、コンテンツ読み込み時に大きくレイアウトがずれ、ユーザーが誤タップしてしまう原因に。

さらに、モバイルでは通信速度が遅く画像の読み込みに時間がかかるため、レイアウトシフトの影響が顕著に現れます。

CLSスコアが悪いとSEO順位低下だけでなく、直帰率上昇やコンバージョン率低下など、ビジネスに直接的な悪影響を及ぼします。

参考記事:CLSとは?

レスポンシブデザインでCLSが悪化しやすい3つの原因

レスポンシブでCLSが悪化しやすい原因をまとめます。

1. 画像の遅延読み込みとサイズ未指定

レスポンシブ画像で最も多いCLS原因が、width・height属性の未指定です。

HTMLに画像サイズを記述せず、CSSでwidth: 100%; height: auto;のみ設定すると、ブラウザは画像ダウンロード完了まで高さを計算できません。

その結果、画像表示の瞬間に下のコンテンツが大きく押し下げられます。

特にファーストビューの大きな画像や、lazy loading(遅延読み込み)を使用している場合、スクロール時に次々とレイアウトシフトが発生。

モバイル環境では通信速度が遅いため、この問題がより深刻化します。

解決には、HTML側でwidth・height指定とaspect-ratioの併用が必須です。

参考記事:遅延読み込みとは?レスポンシブ画像とは

2. 広告・埋め込みコンテンツのサイズ変動

Google AdSenseやYouTube、Twitter埋め込みなど、外部コンテンツは読み込みタイミングが不規則で、CLSの主要原因となります。

広告は入札結果によってサイズが変動し、埋め込みコンテンツはJavaScript読み込み完了後に突然表示されるため、事前にスペース確保しないと大きなレイアウトシフトが発生します。

特にレスポンシブ広告は画面幅に応じてサイズが変わるため、デバイスごとの適切な高さ設定が必要です。

解決策として、min-heightでコンテナの最小高を指定し、aspect-ratioで埋め込みの縦横比を固定することで、読み込み前からスペースを確保できます。

3. 動的コンテンツの挿入タイミング

Webフォント、Cookie同意バナー、通知バー、ポップアップなど、ページ読み込み後に動的挿入される要素はCLSの大きな原因です。

特にWebフォントは、読み込み完了時に文字サイズや行の高さが変化し、ページ全体のレイアウトが崩れます。

JavaScriptで追加される要素も、挿入位置が上部の場合、既存コンテンツを下に押し下げてしまいます。

レスポンシブサイトでは、デバイスごとにフォントサイズや要素の配置が異なるため、影響範囲が予測困難です。

対策として、font-display: swapの使用、固定要素へのposition指定、Skeletonスクリーンによる事前スペース確保が有効です。

レスポンシブのCLS改善①:画像最適化

画像はレスポンシブサイトにおけるCLS問題の最大要因であり、適切な最適化で劇的な改善が可能です。

width・height属性の指定、aspect-ratioプロパティの活用、srcset・sizes属性による適切な画像配信など、実装すべき対策は多岐にわたります。

HTMLとCSSを組み合わせた正しい実装方法を、コード例とともに段階的に解説します。

画像にwidth・heightを指定する

レスポンシブサイトでCLSを改善する最も基本的かつ効果的な方法が、すべての画像にwidth・height属性を指定することです。

ブラウザはこれらの属性から画像のアスペクト比を事前計算し、画像読み込み前から適切なスペースを確保します。

CSSでwidth: 100%; height: auto;を設定しても、HTML属性で元サイズを指定していれば、ブラウザは自動的に縦横比を維持しながらレスポンシブ対応します。

これにより画像読み込み時のレイアウトシフトを完全に防止できます。

古いブラウザでも動作する確実な方法で、実装も簡単。すべての画像に必ず指定しましょう。

特にファーストビューの大きな画像は、CLS改善効果が顕著に現れます。

HTMLサンプルコード(良い例・悪い例)

<!-- 悪い例 -->
<img src="image.jpg" alt="説明">

<!-- 良い例 -->
<img src="image.jpg" alt="説明" width="800" height="600">

CSSサンプルコード(良い例・悪い例)

//悪い例
img {
  width: 100%;
  height: auto;
}

//良い例
img {
  width: 100%;
  height: auto;
  display: block; /* 余白防止 */
}

レスポンシブ画像でのサイズ指定方法

レスポンシブデザインでは、画面幅に応じて画像サイズが変化しますが、CLSを防ぐには適切なサイズ指定が不可欠です。

HTMLのwidth・height属性で元サイズを指定し、CSSでレスポンシブ化する「ハイブリッド方式」が最適解です。

最新のaspect-ratioプロパティを使えば、より明示的にアスペクト比を指定でき、古いブラウザでもHTML属性からアスペクト比が自動計算されます。

srcset・sizes属性と組み合わせることで、デバイスごとに最適な画像を配信しながら、すべてのケースでCLSを防止できます。

重要なのは、画像の表示サイズではなく「アスペクト比」を事前に確定させること。これによりブラウザが読み込み前からスペースを正確に計算し、レイアウトシフトを完全に防ぎます。

aspect-ratioを使った明示的指定

CSS aspect-ratioプロパティは、要素のアスペクト比を直接指定できる最新の手法です。

HTML

<img src="hero-image.jpg" alt="メインビジュアル" width="1920" height="1080">

CSS

.hero-image {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9; /* 明示的に指定 */
  object-fit: cover; /* 切り抜き方法 */
}

HTML属性に依存せず、より柔軟にレイアウト制御が可能で、画像だけでなくあらゆる要素に適用できます。

レスポンシブ対応とCLS防止を両立する最もモダンな方法として推奨されます。

srcset・sizes属性との組み合わせ

srcset・sizes属性を使えば、デバイスの画面幅や解像度に応じて最適な画像を自動配信できます。

width・height属性とaspect-ratioを併用することで、どの画像が読み込まれてもアスペクト比を維持し、CLS完全防止とパフォーマンス最適化を同時に実現できます。

HTML

<img src="image-800w.jpg"
     srcset="image-400w.jpg 400w,
             image-800w.jpg 800w,
             image-1200w.jpg 1200w,
             image-1600w.jpg 1600w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 50vw,
            800px"
     alt="レスポンシブ画像" width="1600" height="900">

CSS

img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

遅延読み込み(Lazy Loading)の正しい実装

遅延読み込みは、スクロールして表示領域に入るまで画像の読み込みを延期する技術で、初期表示速度を大幅に向上させます。

しかし、不適切な実装はCLSを悪化させる主要因です。

重要なのは、lazyload対象の画像でもwidth・height属性を必ず指定すること。

これにより、画像が読み込まれる前からブラウザが適切なスペースを確保し、表示時のレイアウトシフトを防ぎます。

基本実装コード例(loading="lazy")

<!-- ファーストビューの画像:lazyloadなし -->
<img src="hero-image.jpg" 
     alt="メインビジュアル" 
     width="1920" 
     height="1080"
     fetchpriority="high">

<!-- スクロール後の画像:lazyload適用 -->
<img src="content-image.jpg" 
     alt="コンテンツ画像" 
     width="1200" 
     height="800"
     loading="lazy">

CSS例

img {
  width: 100%;
  height: auto;
  aspect-ratio: attr(width) / attr(height);
  display: block;
}

HTML標準のloading="lazy"属性が最もシンプルで推奨される方法です。ブラウザが自動的に最適なタイミングで読み込みを開始します。

より高度な制御が必要な場合は、IntersectionObserver APIを使用しますが、必ずプレースホルダーやaspect-ratio指定でスペースを事前確保しましょう。

注意点として、ファーストビューの画像には絶対にlazyloadを適用しないこと。LCP(最大コンテンツの描画)スコアが悪化します。

レスポンシブのCLS改善②:広告・埋め込みコンテンツ最適化

広告やYouTube、SNS埋め込みなど、外部コンテンツは読み込みタイミングが予測困難で、レスポンシブサイトにおけるCLS悪化の最大要因です。

特にGoogle AdSenseなどのレスポンシブ広告は、デバイスや入札状況によってサイズが変動するため、適切なスペース確保が必須です。

本セクションでは、min-heightによる広告枠の事前確保、YouTube埋め込みのアスペクト比固定テクニック、Twitter・Instagram等のSNSコンテンツ最適化まで、実装コード付きで詳しく解説します。

広告スペースの事前確保

広告は読み込み完了まで実際のサイズが確定しないため、表示時に突然スペースが出現し、大きなレイアウトシフトを引き起こします。

特にレスポンシブ広告は、デバイス幅や入札結果によってサイズが変動するため、CLS対策が困難です。

解決策は、広告コンテナに「最小高さ(min-height)」を事前設定することです。

固定サイズ広告の実装例

<!-- 300x250 レクタングル広告 -->
<div class="ad-container ad-rectangle">
  <ins class="adsbygoogle"
       style="display:block"
       data-ad-client="ca-pub-xxxxx"
       data-ad-slot="xxxxx"
       data-ad-format="rectangle"></ins>
</div>

CSS例

.ad-container {
  width: 100%;
  background-color: #f9f9f9;
  border: 1px solid #e0e0e0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
  position: relative;
}

/* 広告読み込み前の表示 */
.ad-container::before {
  content: 'Advertisement';
  font-size: 12px;
  color: #999;
  position: absolute;
  top: 10px;
  left: 10px;
}

/* 300x250サイズの広告 */
.ad-rectangle {
  min-height: 250px;
  max-width: 300px;
  margin-left: auto;
  margin-right: auto;
}

/* 広告が読み込まれた後、ラベルを非表示 */
.ad-container:has(.adsbygoogle[data-ad-status="filled"])::before {
  display: none;
}

一般的な広告サイズ(300×250、336×280など)を基準に、デバイスごとに適切な高さを確保します。

広告が読み込まれない場合でも空白スペースが表示されますが、CLSを防ぐことでユーザー体験は大幅に向上します。

レスポンシブ広告の実装例

<div class="ad-container ad-responsive">
  <ins class="adsbygoogle"
       style="display:block"
       data-ad-client="ca-pub-xxxxx"
       data-ad-slot="xxxxx"
       data-ad-format="auto"
       data-full-width-responsive="true"></ins>
</div>

CSS例

.ad-responsive {
  width: 100%;
  background-color: #fafafa;
  border: 1px dashed #ddd;
  margin: 30px 0;
  padding: 10px;
  box-sizing: border-box;
}

/* モバイル:小さめの広告 */
@media (max-width: 767px) {
  .ad-responsive {
    min-height: 280px; /* 300x250 + padding */
  }
}

/* タブレット:中サイズ広告 */
@media (min-width: 768px) and (max-width: 1023px) {
  .ad-responsive {
    min-height: 320px; /* 336x280 + padding */
  }
}

/* デスクトップ:大サイズ広告 */
@media (min-width: 1024px) {
  .ad-responsive {
    min-height: 320px; /* 728x90 または 336x280 */
  }
}

モバイルとデスクトップでメディアクエリを使い分け、それぞれ最適な高さを設定。

背景色やローディング表示を追加することで、広告領域であることを視覚的に示し、違和感を軽減できます。

Google AdSenseの場合、data-ad-format属性との組み合わせで、より正確なサイズ予測が可能になります。

YouTubeなど外部埋め込みのアスペクト比固定

YouTube、Google Mapsなどの外部埋め込みコンテンツは、iframeで実装されるため、サイズ指定がないと読み込み時に大きなレイアウトシフトが発生します。

特にレスポンシブ対応では、画面幅に応じてサイズが変化するため、適切なアスペクト比の固定が必須です。

解決策は、「padding-bottom」または「aspect-ratio」を使った固定テクニックです。

padding-bottomを使う従来の方法は、パーセンテージで高さを確保し、子要素のiframeを絶対配置で埋め込みます。

レスポンシブ対応の完全版(HTML)

<div class="responsive-video">
  <div class="video-inner">
    <iframe src="https://www.youtube.com/embed/VIDEO_ID" 
            title="動画タイトル"
            loading="lazy"
            allowfullscreen></iframe>
  </div>
</div>

CSS例

.responsive-video {
  width: 100%;
  max-width: 1200px; /* 最大幅を制限 */
  margin: 30px auto;
  padding: 0 20px;
  box-sizing: border-box;
}

.video-inner {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: #000;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.video-inner iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* モバイルでは余白を小さく */
@media (max-width: 768px) {
  .responsive-video {
    padding: 0 10px;
    margin: 20px auto;
  }
}

16:9の動画ならpadding-bottom: 56.25%(9÷16×100)を設定します。

最新のaspect-ratioプロパティを使えば、よりシンプルに実装可能。

コンテナに直接aspect-ratio: 16 / 9を指定するだけで、レスポンシブ対応とCLS防止を同時に実現できます。

どちらの方法も、iframe読み込み前からスペースが確保され、レイアウトシフトを完全に防ぎます。

SNS埋め込み(Twitter/Instagram)のCLS防止策

TwitterやInstagramの埋め込みは、JavaScriptで動的にコンテンツを生成するため、高さが可変で予測困難です。

ツイートの文字数や画像の有無、リプライの表示などにより高さが大きく変動し、読み込み完了時に突然スペースが出現してCLSを引き起こします。

対策の基本は「min-height」による最小高さの事前確保です。

一般的なツイートは400〜600px、Instagram投稿は600〜800pxを想定して設定します。

Twitter埋め込みの基本実装例

Twitter埋め込みでCLSを防ぐには、コンテナにmin-heightを設定してスペースを事前確保します。

標準的なツイートは400〜500pxを想定し、背景色を設定することで読み込み中も違和感なく表示できます。

HTMLソース例

<div class="twitter-container">
  <blockquote class="twitter-tweet" data-theme="light" data-width="550">
    <p lang="ja" dir="ltr">ツイート本文がここに表示されます</p>
    <a href="https://twitter.com/username/status/1234567890"></a>
  </blockquote>
  <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>

CSS例

.twitter-container {
  position: relative;
  width: 100%;
  max-width: 550px;
  min-height: 500px; /* 標準的なツイートの高さを確保 */
  margin: 30px auto;
  padding: 20px;
  background-color: #f7f9fa; /* Twitter風の背景色 */
  border: 1px solid #e1e8ed;
  border-radius: 12px;
  box-sizing: border-box;
}

/* 読み込み中の表示 */
.twitter-container::before {
  content: 'Loading tweet...';
  display: block;
  color: #657786;
  font-size: 14px;
  text-align: center;
  padding-top: 50px;
}

/* ツイート読み込み完了後、プレースホルダー非表示 */
.twitter-container:has(.twitter-tweet-rendered)::before {
  display: none;
}

/* モバイル対応 */
@media (max-width: 768px) {
  .twitter-container {
    max-width: 100%;
    min-height: 450px;
    padding: 15px;
  }
}

Instagram埋め込みの基本実装例

Instagram埋め込みは、JavaScriptで動的に生成されるため、読み込み完了時に高さが確定します。

一般的な投稿は600〜800px程度ですが、キャプションの長さや画像の有無により変動するため、余裕を持ったmin-height設定が重要です。

基本的な対策として、コンテナに最小高さ(min-height: 700px程度)を指定し、Instagram特有の背景色(#fafafa)とボーダーを設定します。

これにより、読み込み前からスペースが確保され、レイアウトシフトを防止できます。

HTMLソース例

<div class="instagram-container">
  <blockquote class="instagram-media" 
              data-instgrm-captioned 
              data-instgrm-permalink="https://www.instagram.com/p/xxxxx/"
              data-instgrm-version="14">
    <a href="https://www.instagram.com/p/xxxxx/"></a>
  </blockquote>
  <script async src="//www.instagram.com/embed.js"></script>
</div>

CSS例

.instagram-container {
  position: relative;
  width: 100%;
  max-width: 540px;
  min-height: 700px; /* Instagram投稿の標準的な高さ */
  margin: 30px auto;
  background-color: #fafafa;
  border: 1px solid #dbdbdb;
  border-radius: 3px;
  padding: 0;
}

/* 読み込み中の表示 */
.instagram-container::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
  border: 3px solid #dbdbdb;
  border-top-color: #405de6; /* Instagramブランドカラー */
  border-radius: 50%;
  animation: instagram-spinner 0.8s linear infinite;
}

@keyframes instagram-spinner {
  to { transform: translate(-50%, -50%) rotate(360deg); }
}

/* Instagram読み込み完了後、スピナー非表示 */
.instagram-container:has(.instagram-media-rendered)::before {
  display: none;
}

/* モバイル対応 */
@media (max-width: 768px) {
  .instagram-container {
    max-width: 100%;
    min-height: 650px;
  }
}

動的コンテンツ読み込みによるCLS改善策

Webフォント、Cookie同意バナー、通知バー、ポップアップなど、JavaScriptで動的に挿入される要素は、ページ読み込み後に突然表示されるためCLSの大きな原因となります。

特にページ上部に追加される要素は、既存コンテンツ全体を下に押し下げ、ユーザーが読んでいた位置を見失わせます。

下記に改善策をご説明します。

Webフォント読み込みの最適化

Webフォントは、読み込み完了時に文字サイズや行の高さが変化し、レイアウト全体に影響します。

font-display: swapやoptionalプロパティを使用し、フォント読み込み時の挙動を制御することが重要です。

システムフォントへの適切なフォールバック設定も必須です。

font-displayの使用

font-displayプロパティで読み込み時の挙動を制御します。

「swap」は即座にシステムフォントを表示し、読み込み完了後に置き換え。

「optional」はネットワーク状況が悪い場合、フォント読み込みをスキップしてCLSを完全防止します。

HTML例

<!-- フォントのプリロード(推奨) -->
<link rel="preload" 
      href="/fonts/NotoSansJP-Regular.woff2" 
      as="font" 
      type="font/woff2" 
      crossorigin>

CSS例

@font-face {
  font-family: 'Noto Sans JP';
  src: url('/fonts/NotoSansJP-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* または optional */
}

body {
  font-family: 'Noto Sans JP', 
               -apple-system, 
               BlinkMacSystemFont, 
               'Segoe UI', 
               'Hiragino Sans', 
               'Hiragino Kaku Gothic ProN', 
               'メイリオ', 
               Meiryo, 
               sans-serif;
}

optional使用

font-display: optionalは、CLS完全防止に最も効果的な設定です。

フォント読み込みに100ms以上かかる場合、自動的にシステムフォントを使用し続けるため、レイアウトシフトが発生しません。

通信速度が速い環境では即座にWebフォントが適用され、遅い環境ではシステムフォントのまま表示されます。

CSS例

@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom-font.woff2') format('woff2');
  font-weight: 400;
  font-display: optional; /* CLSを最小化 */
}

body {
  font-family: 'Custom Font', system-ui, -apple-system, sans-serif;
}

ユーザー体験を損なわず、パフォーマンスとCLSスコアを両立できる最適解です。

特にモバイル環境や、Core Web Vitalsを重視するサイトで推奨されます。デザインの一貫性よりもパフォーマンスを優先する場合に最適な選択肢です。

バナーや通知バーの表示制御

Cookie同意バナー、プロモーション通知、ニュース速報バーなど、ページ上部に動的に表示される要素は、既存コンテンツを下に押し下げて大きなレイアウトシフトを引き起こします。

特にファーストビューのコンテンツが移動すると、ユーザーがクリックしようとした瞬間にボタンがずれる「誤タップ」が発生し、UXを著しく損ないます。

具体的な対策を下記にまとめていきます。

position: fixed を使った基本実装

最も効果的な対策は、position: fixedまたはabsoluteで固定配置し、通常のドキュメントフローから外すことです。

これにより、バナーが表示されても他のコンテンツは移動しません。

上部に表示する場合、bodyやmainコンテンツにpadding-topを事前設定してスペースを確保します。

アニメーション表示する場合は、transformとopacityプロパティのみを使用します。

topやheightの変更はリフローを引き起こしCLSの原因となるため避けましょう。

さらに、初回訪問時にのみ表示する、スクロール後に表示するなど、表示タイミングを工夫することでCLS影響を最小化できます。

HTML例

<!-- Cookie同意バナー -->
<div class="cookie-banner" id="cookieBanner">
  <div class="cookie-content">
    <p>このサイトはCookieを使用しています。</p>
    <button class="cookie-accept" onclick="acceptCookie()">同意する</button>
  </div>
</div>

<main class="main-content">
  <!-- メインコンテンツ -->
</main>

CSS例

/* 悪い例:通常配置(CLSが発生) */
.cookie-banner-bad {
  width: 100%;
  background-color: #333;
  padding: 15px;
  /* position指定なし = 他のコンテンツを押し下げる */
}

/* 良い例:固定配置(CLS防止) */
.cookie-banner {
  position: fixed;
  bottom: 0; /* または top: 0 */
  left: 0;
  width: 100%;
  background-color: #333;
  color: #fff;
  padding: 15px 20px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
  z-index: 1000;
  /* 初期状態で非表示(transformで制御) */
  transform: translateY(100%);
  transition: transform 0.3s ease;
}

/* 表示状態 */
.cookie-banner.show {
  transform: translateY(0);
}

.cookie-content {
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

.cookie-accept {
  padding: 10px 24px;
  background-color: #4CAF50;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
}

/* モバイル対応 */
@media (max-width: 768px) {
  .cookie-content {
    flex-direction: column;
    text-align: center;
  }
  
  .cookie-accept {
    width: 100%;
  }
}

javascript例

// バナー表示制御
function showCookieBanner() {
  const banner = document.getElementById('cookieBanner');
  
  // Cookieが未設定の場合のみ表示
  if (!getCookie('cookie_accepted')) {
    // DOMContentLoaded後に表示(CLS回避)
    setTimeout(() => {
      banner.classList.add('show');
    }, 100);
  }
}

function acceptCookie() {
  // Cookie設定
  setCookie('cookie_accepted', 'true', 365);
  
  // バナー非表示
  const banner = document.getElementById('cookieBanner');
  banner.classList.remove('show');
}

// ページ読み込み時に実行
document.addEventListener('DOMContentLoaded', showCookieBanner);

無限スクロール・ページネーションの実装

無限スクロールやページネーション機能は、ユーザーがスクロールすると新しいコンテンツを動的に追加する仕組みです。

しかし、コンテンツ追加時にサイズが確定していないと、画像や広告の読み込み完了時に大きなレイアウトシフトが発生し、ユーザーが見ていた位置がずれてしまいます。

下記にそれぞれの改善策をご紹介します。

Skeletonスクリーン付き無限スクロールの設置について

最も効果的な対策は、Skeletonスクリーン(スケルトンスクリーン)の活用です。

Skeletonスクリーンは、コンテンツ読み込み前にグレーのアニメーション枠を表示し、実際のコンテンツと同じスペースを事前確保する手法です。

ユーザーに「読み込み中」を視覚的に伝えながら、CLSを完全に防止します。

実装は、IntersectionObserver APIで画面下部到達を監視し、200px手前で読み込み開始。

Skeleton表示中にAPIからデータ取得し、完了後に実コンテンツと置き換えます。

各Skeletonカードは実際のカードと同じmin-heightとaspect-ratioを指定し、グラデーションアニメーションで読み込み状態を表現します。

最低500msは表示してちらつきを防止。無限スクロールで最もCLS対策効果が高い実装方法です。

HTML例

<div class="content-container" id="contentContainer">
  <!-- 既存コンテンツ -->
  <article class="content-card">
    <img src="image1.jpg" alt="記事1" width="400" height="300">
    <h3>記事タイトル1</h3>
    <p>記事の説明文...</p>
  </article>
  
  <!-- 他のコンテンツ... -->
</div>

<!-- ローディング表示エリア -->
<div class="loading-container" id="loadingContainer">
  <!-- Skeletonスクリーンがここに挿入される -->
</div>

<!-- 監視用のセンチネル要素 -->
<div class="scroll-sentinel" id="scrollSentinel"></div>

CSS例

.content-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 30px;
}

/* コンテンツカード */
.content-card {
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: box-shadow 0.2s;
}

.content-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.content-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3; /* CLS防止 */
  object-fit: cover;
  display: block;
}

.content-card h3 {
  padding: 15px 15px 10px;
  margin: 0;
  font-size: 18px;
  color: #333;
}

.content-card p {
  padding: 0 15px 15px;
  margin: 0;
  font-size: 14px;
  color: #666;
  line-height: 1.6;
}

/* ローディングコンテナ */
.loading-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 30px;
}

/* Skeletonスクリーン */
.skeleton-card {
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  /* 最小高さを確保(CLS防止) */
  min-height: 350px;
}

.skeleton-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
}

.skeleton-title {
  margin: 15px 15px 10px;
  height: 20px;
  width: 80%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
  border-radius: 4px;
}

.skeleton-text {
  margin: 0 15px 8px;
  height: 14px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
  border-radius: 4px;
}

.skeleton-text:last-child {
  width: 60%;
  margin-bottom: 15px;
}

@keyframes skeleton-shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

/* センチネル要素(監視用) */
.scroll-sentinel {
  height: 1px;
  visibility: hidden;
}

javascript例

// 無限スクロール実装
class InfiniteScroll {
  constructor() {
    this.page = 1;
    this.loading = false;
    this.hasMore = true;
    this.itemsPerPage = 6;
    
    this.init();
  }
  
  init() {
    // IntersectionObserverで画面下部到達を監視
    const sentinel = document.getElementById('scrollSentinel');
    
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting && !this.loading && this.hasMore) {
            this.loadMore();
          }
        });
      },
      {
        rootMargin: '200px' // 200px手前で読み込み開始
      }
    );
    
    observer.observe(sentinel);
  }
  
  async loadMore() {
    this.loading = true;
    
    // Skeletonスクリーン表示
    this.showSkeletons();
    
    try {
      // APIからデータ取得(シミュレーション)
      const data = await this.fetchData(this.page + 1);
      
      // 最低500msは表示(ちらつき防止)
      await new Promise(resolve => setTimeout(resolve, 500));
      
      // Skeleton削除
      this.hideSkeletons();
      
      // 実際のコンテンツ追加
      this.appendContent(data);
      
      this.page++;
      
      // データがなくなったら終了
      if (data.length < this.itemsPerPage) {
        this.hasMore = false;
        this.showEndMessage();
      }
      
    } catch (error) {
      console.error('Loading failed:', error);
      this.hideSkeletons();
      this.showErrorMessage();
    } finally {
      this.loading = false;
    }
  }
  
  showSkeletons() {
    const container = document.getElementById('loadingContainer');
    container.innerHTML = '';
    
    // 6個のSkeletonカードを表示
    for (let i = 0; i < this.itemsPerPage; i++) {
      const skeleton = document.createElement('div');
      skeleton.className = 'skeleton-card';
      skeleton.innerHTML = `
        <div class="skeleton-image"></div>
        <div class="skeleton-title"></div>
        <div class="skeleton-text"></div>
        <div class="skeleton-text"></div>
        <div class="skeleton-text"></div>
      `;
      container.appendChild(skeleton);
    }
  }
  
  hideSkeletons() {
    const container = document.getElementById('loadingContainer');
    container.innerHTML = '';
  }
  
  async fetchData(page) {
    // API呼び出しをシミュレーション
    const response = await fetch(`/api/content?page=${page}&limit=${this.itemsPerPage}`);
    return await response.json();
  }
  
  appendContent(data) {
    const container = document.getElementById('contentContainer');
    
    data.forEach(item => {
      const card = document.createElement('article');
      card.className = 'content-card';
      card.innerHTML = `
        <img src="${item.image}" 
             alt="${item.title}" 
             width="400" 
             height="300"
             loading="lazy">
        <h3>${item.title}</h3>
        <p>${item.description}</p>
      `;
      container.appendChild(card);
    });
  }
  
  showEndMessage() {
    const container = document.getElementById('loadingContainer');
    container.innerHTML = '<p style="text-align: center; color: #999; padding: 40px;">すべてのコンテンツを読み込みました</p>';
  }
  
  showErrorMessage() {
    const container = document.getElementById('loadingContainer');
    container.innerHTML = `
      <div style="text-align: center; padding: 40px;">
        <p style="color: #f44336; margin-bottom: 15px;">読み込みに失敗しました</p>
        <button onclick="infiniteScroll.loadMore()" style="padding: 10px 20px; background: #2196F3; color: #fff; border: none; border-radius: 4px; cursor: pointer;">再試行</button>
      </div>
    `;
  }
}

// 初期化
const infiniteScroll = new InfiniteScroll();

さらに、IntersectionObserver APIを使用してスクロール位置を監視し、画面下部に到達する直前(例:200px手前)で読み込みを開始することで、ユーザーが待つことなくスムーズな体験を提供できます。

追加するコンテンツの各要素(画像、カード等)には必ずwidth・height・aspect-ratioを指定し、個別の要素でもCLSを防止しましょう。

CSS・レイアウトによるCLS対策

CSSプロパティの選択とレイアウト手法は、CLSに直接的な影響を与えます。

topやleftなどの位置プロパティを変更すると、ブラウザがレイアウト全体を再計算(リフロー)し、パフォーマンス低下とレイアウトシフトを引き起こします。

ここでは、リフローを回避するCSSプロパティの使い分け、レイアウトシフトを防ぐFlexbox・Grid設定、最新のcontent-visibilityプロパティによるレンダリング最適化まで、CSS視点からのCLS改善テクニックを解説します。

CSSのtransformとopacityを優先使用

transformとopacityは、GPU(グラフィックス処理装置)で処理される「合成レイヤー」上で動作するため、レイアウト全体の再計算(リフロー)を引き起こしません。

そのため、アニメーションやトランジション実装時にCLSを完全に防止できます。

逆に、top・left・width・heightなどのプロパティを変更すると、ブラウザがページ全体のレイアウトを再計算し、パフォーマンスが低下します。

要素を移動させる場合はtopではなくtransform: translateを、表示・非表示にはdisplayではなくopacityとvisibilityの組み合わせを使用します。

これにより60fpsの滑らかなアニメーションを維持しながら、CLSスコアも最適化できます。モダンブラウザでは必須のベストプラクティスです。

CSS例

/* 悪い例 - リフローを引き起こす */
.element {
  top: 100px;
  left: 50px;
}

/* 良い例 - レイヤー化される */
.element {
  transform: translate(50px, 100px);
}

コンテンツの可視性制御テクニック

content-visibilityは、画面外のコンテンツのレンダリングをスキップし、初期表示速度を劇的に向上させる最新CSSプロパティです。

ブラウザは表示領域外の要素のレイアウト計算を省略し、スクロールで近づいた時だけレンダリングします。

特に長いページや大量の画像・複雑なレイアウトを持つコンテンツで効果的です。

contain-intrinsic-sizeと併用することで、レンダリング前でも要素の高さを確保し、CLSを防止できます。

これにより、パフォーマンスとCLS対策を同時に実現します。

実装例(HTML)

<article class="blog-post">
  <h2>記事タイトル1</h2>
  <div class="post-content">
    <p>記事の内容...</p>
    <img src="image1.jpg" alt="画像" width="800" height="600">
  </div>
</article>

<article class="blog-post">
  <h2>記事タイトル2</h2>
  <div class="post-content">
    <p>記事の内容...</p>
    <img src="image2.jpg" alt="画像" width="800" height="600">
  </div>
</article>

<!-- 以下、多数の記事... -->

CSS例

/* content-visibilityの基本使用 */
.blog-post {
  /* 画面外のコンテンツはレンダリングスキップ */
  content-visibility: auto;
  
  /* レンダリング前の高さを指定(CLS防止) */
  contain-intrinsic-size: auto 500px;
  
  /* その他のスタイル */
  margin-bottom: 40px;
  padding: 20px;
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

ただし、検索エンジンのクロール、アクセシビリティへの影響に注意が必要です。

適切に使用すれば、初期レンダリング時間を50%以上削減できる強力な最適化手法です。

Flexbox・Gridレイアウトでの注意点

FlexboxとGrid Layoutは強力なレスポンシブレイアウト手法ですが、子要素のサイズを明示的に指定しないと、コンテンツ読み込み時に予期しない伸縮が発生しCLSの原因となります。

特に画像や動的コンテンツを含む場合、問題が顕著です。

Flexboxでは、flex-basisやmin-width/max-widthで子要素の基本サイズを明示的に設定し、flex-shrink: 0で縮小を防ぎます。

flex: 1のような省略形は便利ですが、コンテンツ量によってサイズが変動するため注意が必要です。

画像を含むflex子要素には必ずaspect-ratioを指定しましょう。

Grid Layoutでは、grid-template-columnsにauto-fillやauto-fitを使う際、minmax()関数で最小・最大サイズを明確に定義します。

grid-auto-rowsで行の高さを事前設定し、画像などのコンテンツには明示的な高さまたはaspect-ratioを指定することで、グリッドアイテムの読み込み時のレイアウトシフトを防止できます。

JavaScriptによるCLS改善アプローチ

JavaScriptはページに動的な機能を追加する一方で、不適切な実装はCLS悪化の大きな原因となります。

DOM要素の動的挿入、スタイルの頻繁な変更、スクリプト実行タイミングの問題など、JavaScript起因のレイアウトシフトは多岐にわたります。

本セクションでは、効率的なDOM操作テクニック、レンダリングパフォーマンスを損なわないJavaScript実装パターン、Web Performance APIを活用したCLSのリアルタイム監視などを解説します。

DOMContentLoadedとレンダリングタイミング

DOMContentLoadedイベントはHTML解析完了時、loadイベントは全リソース(画像・CSS等)読み込み完了時に発火します。

この違いを理解し適切に使い分けることがCLS対策の鍵です。

DOMContentLoaded前にスクリプトを実行すると、ファーストビューのレンダリングをブロックします。

逆にload後では遅すぎて、ユーザーが既に操作を開始している可能性があります。

CLS防止には、スクリプトタグにasync・defer属性を使用し、非同期読み込みを実現します。

適切なタイミングでのDOM操作(CLS防止)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>タイミング制御デモ</title>
  
  <!-- 悪い例:レンダリングブロック -->
  <script src="heavy-script.js"></script>
  
  <!-- 良い例:defer(DOM操作に最適) -->
  <script defer src="dom-script.js"></script>
  
  <!-- 良い例:async(独立したスクリプト) -->
  <script async src="analytics.js"></script>
</head>
<body>
  <div id="content">
    <h1>コンテンツ</h1>
  </div>
</body>
</html>

deferはHTML解析後・DOMContentLoaded前に実行され、DOM操作が必要なスクリプトに最適です。

defer属性の使用例

<!-- defer: HTML解析を妨げず、DOMContentLoaded前に順序通り実行 -->
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
<script defer src="script3.js"></script>

asyncは読み込み完了次第実行されるため、順序が重要でない独立したスクリプトに適しています。

async属性の使用例

<!-- async: 読み込み完了次第すぐ実行(順序不定) -->
<script async src="analytics.js"></script>
<script async src="ad-script.js"></script>

適切なタイミング制御でCLSを防ぎつつ、高速なページ表示を実現できます。

動的なスタイル変更の最小化

JavaScriptで複数のスタイルを変更すると、変更のたびにブラウザがレイアウト再計算(リフロー)を実行し、パフォーマンスが著しく低下します。

特にループ内でのスタイル変更や、頻繁な読み取り・書き込みの混在は重大なボトルネックです。

改善策は「バッチ処理」です。複数のスタイル変更を一度にまとめて実行し、リフローを1回に抑えます。

CSSクラスの追加・削除でまとめて変更する、DocumentFragmentで要素を事前構築してから一括挿入する、cssTextで複数プロパティを一度に設定する手法が有効です。

さらに、requestAnimationFrame(rAF)を使用すれば、ブラウザの次の描画タイミングに合わせてスタイル変更を実行し、無駄なリフローを排除できます。

レイアウト情報(offsetHeight等)の読み取りと書き込みを分離し、「読み取り→書き込み」の順序で実行することで、強制同期レイアウト(forced reflow)を回避し、CLSを最小化しながら滑らかなアニメーションを実現できます。

Web Performance APIでのモニタリング

Web Performance APIを使用すると、CLSをリアルタイムで検出・測定し、問題箇所を特定できます。

PerformanceObserverでlayout-shiftエントリを監視し、レイアウトシフトが発生するたびに詳細情報を取得します。

CLSスコアは各シフトの影響度を累積計算した値で、0.1以下が良好とされます。

hadRecentInputプロパティで、ユーザー操作直後のシフト(スコアに含めない)を除外できます。

実際の本番環境でCLSデータを収集し、アナリティクスに送信することで、ユーザーが体験している問題を正確に把握できます。

この情報を元に優先順位をつけて改善を進めることで、効率的なCLS最適化が実現します。

開発環境だけでなく、実ユーザーデータの継続的な監視が重要です。

レスポンシブサイトのCLS改善まとめ

レスポンシブサイトでのCLS改善は、一見複雑に見えますが、基本原則はシンプルです。

「すべての要素のサイズを事前に確保する」これだけを徹底すれば、必ず結果が出ます。

一つひとつは小さな改善ですが、積み重ねることでCLSスコア0.1以下は必ず達成できます。

ユーザー体験の向上、SEO順位の改善、コンバージョン率の向上という大きな成果が待っています。

今日から一歩ずつ、一緒にCLS改善に取り組んでいきましょう!

記事を書いた人

井上寛生

井上寛生

LandingHub 執行役員 / 事業責任者 / 技術責任者

大学院では情報工学を専攻し、修了後に株式会社TeNへ新卒入社。当時は社内唯一のエンジニアながら、開発部門をゼロから立ち上げ、採用・育成を一手に担い、全員が未経験からスタートした精鋭エンジニアチームを組成。2021 年にはWEBサイト高速化プラットフォーム「LandingHub」を立ち上げ、プロダクトオーナー兼事業責任者として企画・開発・グロースを牽引。現在は執行役員として、会社の技術戦略と事業成長の双方をリードしている。
コラム一覧に戻る