画像の遅延読み込みは、まず HTML の loading="lazy" を使うのが2026年時点の正解です。全モダンブラウザが対応していて、JavaScriptを1行も書かずに済みます。属性側の書き方と、効かないときの切り分けはloading=”lazy”が効かない原因とデメリットにまとめました。
この記事が扱うのは、そのネイティブ属性では届かない場面だけです。具体的にはCSSの背景画像・重いiframe埋め込み(地図・動画・広告)・自前のプレースホルダー演出・スクロール連動の計測の4つで、ここが Intersection Observer API の担当範囲になります。それぞれの実装をそのまま使える形で載せます。
スポンサーリンク
遅延読み込みで何が改善するのか
結論として、遅延読み込みが効くのは初期表示に必要ないリソースのダウンロードを後回しにできる点です。転送量と、ネットワークの取り合いが減ります。
表示速度が離脱に直結することは、Googleが2017年に公表したモバイル速度のレポートで示されています。
| ページ表示時間 | 直帰率の上昇 |
|---|---|
| 1秒 → 3秒 | 約32%上昇 |
| 1秒 → 5秒 | 約90%上昇 |
| 1秒 → 10秒 | 約123%上昇 |
出典:Google「The need for mobile speed」(2017年)。数値自体は古いですが、傾向は現在のCore Web Vitalsの考え方とも一致しています。
ネイティブ属性で届かないのはどこか
結論は、<img> と <iframe> なら loading="lazy" で終わり、それ以外は Intersection Observer が要るです。先に対象を切り分けてから書き始めてください。
| 対象 | loading="lazy" |
Intersection Observer |
|---|---|---|
| <img> / <iframe> | ◎ これで十分 | 不要 |
| CSSの背景画像 | ✕ 使えない | ◎ クラス付け替えで実現 |
| <video> の読み込み制御 | △ preloadのみ |
◎ 画面内で再生開始など |
| 読み込み時のフェード演出 | ✕ | ◎ |
| 広告・地図などの重いスクリプト | ✕ | ◎ 表示直前に初期化 |
| 要素の表示回数の計測 | ✕ | ◎ |
1行目の <img> / <iframe> については、この記事では扱いません。width / height / decoding="async" の併記、ファーストビューの画像に付けてはいけない理由(LCPが悪化します)、fetchpriority との使い分けはloading=”lazy”が効かない原因とデメリットで解説しています。以下は表の2行目以降を順に実装していきます。
スポンサーリンク
背景画像を 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' が実用上のコツです。画面に入った瞬間に取得を始めると、ユーザーには「遅れて表示された」ように見えます。少し手前から始めることで、体感上は最初から表示されていたのと同じになります。loading="lazy" ではこの距離を指定できないので、先読み量を自分で決めたい場合もこちらになります。
自前のプレースホルダーとフェード演出
<img>に対して data-src 方式を使う必要は、もうありません。 かつて主流だった「data-srcに本物のURLを入れ、画面内に入ったらsrcに移す」方式は、loading="lazy"が広く使えるようになった今では、次の欠点だけが残ります。
- JavaScriptが失敗すると画像が1枚も表示されない
- ブラウザのプリロードスキャナが画像を見つけられず、優先度制御が効かない
srcset/sizesの扱いが煩雑になる
プレースホルダーからふわっと表示させたいだけなら、data-srcも Intersection Observer も要りません。ブラウザに読み込みは任せたまま、loadイベントでクラスを付けるだけで済みます。
img[loading="lazy"] {
background-color: #f2f2f2; /* 読み込み前の下地 */
opacity: 0;
transition: opacity .4s;
}
img[loading="lazy"].is-loaded {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
img[loading="lazy"] { opacity: 1; transition: none; }
}
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.complete のチェックが要ります。キャッシュから即座に読み込まれた画像では、スクリプトが動く前にloadが終わっていることがあるためです。これを忘れると、一部の画像だけ透明のまま残るという厄介なバグになります。
この書き方なら、JavaScriptが動かなくても画像自体はブラウザが読み込むので、最悪でも「フェードしないだけ」で済みます。prefers-reduced-motion: reduce の指定も忘れないでください。動きを減らす設定にしている環境では、フェードそのものが負担になります。
スポンサーリンク
重い埋め込み(地図・動画・広告)を後回しにする
体感速度に一番効くのは、実は画像よりサードパーティの埋め込みです。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にアクセシブルな名前が無いと、スクリーンリーダーで内容を判別できません。
動画も同じ考え方です。<video> には loading 属性が無いため、本体の取得を止める書き方(preload="none" と poster)は属性側の記事に譲ります。画面内に入ってから再生を始めたい、という挙動が要るときだけ、上と同じ形で Intersection Observer を使います。
長いリストの描画負荷を下げる
遅延読み込みで通信は減っても、DOM要素が多いこと自体の負荷は減りません。ここには CSS の content-visibility が効きます。
.card {
content-visibility: auto;
contain-intrinsic-size: auto 240px;
}
画面外の要素はレンダリングがスキップされます。contain-intrinsic-sizeで概算の高さを渡さないとスクロールバーが跳ねるので、必ずセットで指定してください。
コンテンツを順次追加していく無限スクロールの実装方法、要素の出現に合わせたスクロールアニメーションは別記事にまとめています。どちらも同じ Intersection Observer の応用です。
まとめ
- <img> と <iframe> は
loading="lazy"で十分。Intersection Observer は不要(詳細はloading=”lazy”が効かない原因とデメリット) - Intersection Observer が必要なのは、背景画像・重い埋め込み(地図・動画・広告)・自前の演出・計測
- CSSの背景画像は
data-bgをクラス/スタイルに移す方式で遅延読み込みする rootMarginを200〜400px取って先読みすると、遅延読み込みだと気づかれないdata-src方式は<img>にはもう不要。JS失敗時に画像が消えるリスクだけが残る- フェード演出は
loadイベント+img.completeチェックで足りる。prefers-reduced-motionも書く - iframeを後から生成するときは
title属性を必ず付ける - DOMが多いこと自体の負荷には
content-visibility: auto
「まずネイティブ属性、届かないところだけ Intersection Observer」。この順番で考えると、書くコードは最小になります。