Intersection Observer APIで遅延読み込みを最適化する方法
画像の遅延読み込みは、まず HTML の loading="lazy" を使うのが2026年時点の正解です。全モダンブラウザが対応していて、JavaScriptを1行も書かずに済みます。
それでも Intersection Observer API が要るのは、背景画像・動画・iframe埋め込み・自前のプレースホルダー演出・スクロール連動の計測など、loading="lazy" では届かない場面です。
この記事では、まずネイティブ属性で片づく範囲を確定させたうえで、Intersection Observer が必要になる条件と実装、そしてファーストビューの画像を遅延読み込みしてはいけない理由(LCPが悪化します)を解説します。
遅延読み込みで何が改善するのか
結論として、遅延読み込みが効くのは初期表示に必要ないリソースのダウンロードを後回しにできる点です。転送量と、ネットワークの取り合いが減ります。
表示速度が離脱に直結することは、Googleが2017年に公表したモバイル速度のレポートで示されています。
| ページ表示時間 | 直帰率の上昇 |
|---|---|
| 1秒 → 3秒 | 約32%上昇 |
| 1秒 → 5秒 | 約90%上昇 |
| 1秒 → 10秒 | 約123%上昇 |
出典:Google「The need for mobile speed」(2017年)。数値自体は古いですが、傾向は現在のCore Web Vitalsの考え方とも一致しています。
まず loading=”lazy” で足りるか判定する
<img> と <iframe> の遅延読み込みは、属性1つで終わります。 Chrome・Edge・Firefox・Safari のすべてが対応済みです。
<img src="photo.jpg" width="800" height="600" alt="サンプル画像"
loading="lazy" decoding="async">
<iframe src="https://www.youtube.com/embed/xxxx"
width="560" height="315" loading="lazy"
title="紹介動画"></iframe>
あわせて必ず入れたいのが次の3点です。
widthとheightを書く:領域が先に確保され、読み込み時のレイアウトシフト(CLS)を防げますdecoding="async":画像のデコードでメインスレッドを止めません- ファーストビューの画像には
loading="lazy"を付けない:後述しますが、LCPが確実に悪化します
ネイティブ属性で届かないケース
| 対象 | loading="lazy" |
Intersection Observer |
|---|---|---|
| <img> / <iframe> | ◎ これで十分 | 不要 |
| CSSの背景画像 | ✕ 使えない | ◎ クラス付け替えで実現 |
| <video> の読み込み制御 | △ preloadのみ |
◎ 画面内で再生開始など |
| 読み込み時のフェード演出 | ✕ | ◎ |
| 広告・地図などの重いスクリプト | ✕ | ◎ 表示直前に初期化 |
| 要素の表示回数の計測 | ✕ | ◎ |
ファーストビューの画像を遅延読み込みしてはいけない理由
LCP(Largest Contentful Paint)が悪化するからです。 ページ上部のメイン画像に loading="lazy" を付けると、ブラウザはレイアウトが確定するまでその画像の取得を始めません。結果として、最大要素の描画が数百ミリ秒〜数秒遅れます。
ファーストビューの主役画像には、逆に優先度を上げる指定を入れます。
<!-- ページ最上部のメインビジュアル -->
<img src="hero.jpg" width="1200" height="630" alt="メインビジュアル"
fetchpriority="high" decoding="async">
判断基準はシンプルです。
- スクロールせずに見える画像 →
loadingを付けない、必要ならfetchpriority="high" - スクロールしないと見えない画像 →
loading="lazy"
「全部の画像に一括でloading="lazy"を付ける」プラグイン設定はここで足を引っ張ります。除外設定があるか必ず確認してください。
Intersection Observer で背景画像を遅延読み込みする
CSSのbackground-imageはネイティブの遅延読み込みが効きません。ここが Intersection Observer の主戦場です。
<section class="hero-bg" data-bg="/images/scenery.jpg">
<h2>見出し</h2>
</section>
.hero-bg {
min-height: 320px;
background-color: #eee; /* 読み込み前の下地 */
background-size: cover;
background-position: center;
transition: opacity .4s;
}
const targets = document.querySelectorAll('[data-bg]');
const observer = new IntersectionObserver(
(entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const el = entry.target;
el.style.backgroundImage = `url("${el.dataset.bg}")`;
el.removeAttribute('data-bg');
obs.unobserve(el);
}
},
{ rootMargin: '200px' } // 画面に入る200px手前で取得を始める
);
targets.forEach((el) => observer.observe(el));
rootMargin: '200px' が実用上のコツです。画面に入った瞬間に取得を始めると、ユーザーには「遅れて表示された」ように見えます。少し手前から始めることで、体感上は最初から表示されていたのと同じになります。
data-src 方式を今も使うべきか
<img>に対しては、もう不要です。 かつて主流だった「data-srcに本物のURLを入れ、画面内に入ったらsrcに移す」方式は、loading="lazy"が広く使えるようになった今では、次の欠点だけが残ります。
- JavaScriptが失敗すると画像が1枚も表示されない
- ブラウザのプリロードスキャナが画像を見つけられず、優先度制御が効かない
srcset/sizesの扱いが煩雑になる
読み込み時のフェード演出だけが目的なら、data-srcを使わずにloadイベントで済ませられます。
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
if (img.complete) {
img.classList.add('is-loaded');
return;
}
img.addEventListener('load', () => img.classList.add('is-loaded'), { once: true });
});
img[loading="lazy"] {
opacity: 0;
transition: opacity .4s;
}
img[loading="lazy"].is-loaded {
opacity: 1;
}
この書き方なら、JavaScriptが動かなくても画像自体はブラウザが読み込むので、最悪でも「フェードしないだけ」で済みます。
重い埋め込み(地図・動画・広告)を後回しにする
体感速度に一番効くのは、実は画像よりサードパーティの埋め込みです。YouTube や Google マップの iframe は、それだけで数百KBのスクリプトを引き連れてきます。
loading="lazy"を付けるだけでも効きますが、クリックされるまでサムネイル画像で代替する方式が最も軽くなります。
const embed = document.querySelector('#map-placeholder');
const observer = new IntersectionObserver((entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const iframe = document.createElement('iframe');
iframe.src = entry.target.dataset.src;
iframe.width = '100%';
iframe.height = '400';
iframe.loading = 'lazy';
iframe.title = '店舗の地図';
entry.target.replaceWith(iframe);
obs.disconnect();
}
}, { rootMargin: '400px' });
observer.observe(embed);
title属性を必ず付けてください。iframeにアクセシブルな名前が無いと、スクリーンリーダーで内容を判別できません。
長いリストの描画負荷を下げる
遅延読み込みで通信は減っても、DOM要素が多いこと自体の負荷は減りません。ここには CSS の content-visibility が効きます。
.card {
content-visibility: auto;
contain-intrinsic-size: auto 240px;
}
画面外の要素はレンダリングがスキップされます。contain-intrinsic-sizeで概算の高さを渡さないとスクロールバーが跳ねるので、必ずセットで指定してください。
コンテンツを順次追加していく無限スクロールの実装方法、要素の出現に合わせたスクロールアニメーションは別記事にまとめています。ネイティブ属性側の詳細はHTMLのネイティブ Lazy Loading の改善を参照してください。
まとめ
- <img> と <iframe> は
loading="lazy"で十分。JavaScriptは要らない - あわせて
width/height/decoding="async"を書き、CLSを防ぐ - ファーストビューの画像に
loading="lazy"を付けない。LCPが悪化する。むしろfetchpriority="high" - Intersection Observer が必要なのは、背景画像・動画・重い埋め込み・演出・計測
rootMarginを200〜400px取って先読みすると、遅延読み込みだと気づかれないdata-src方式は<img>にはもう不要。JS失敗時に画像が消えるリスクだけが残る- DOMが多いこと自体の負荷には
content-visibility: auto
「まずネイティブ属性、届かないところだけ Intersection Observer」。この順番で考えると、書くコードは最小になります。