【JavaScript】Intersection Observerを使った画像の遅延読み込み方法

画像の遅延読み込み(Lazy Load)は、画面に入った画像だけを読み込んで初期表示を高速化するテクニックです。この記事では Intersection Observer を使った自前の実装を、手を動かしながら解説します。

先に結論:まずネイティブの loading="lazy" で十分か検討してください(JSなしで動きます)。フェードインや先読み距離の調整など細かく制御したいときに、Intersection Observer で自作します。ネイティブ属性・LCPへの影響・各手法の比較はLazyload(遅延読み込み)完全ガイドにまとめています。
スポンサーリンク

最も簡単:loading=”lazy”(JS不要)

多くの場合、HTML標準の loading="lazy" だけで十分です。<img> に属性を1つ足すだけで、ブラウザが自動で遅延読み込みします。まずはこれで足りないか確認しましょう。

HTML:これだけで遅延読み込み
<img src="photo.jpg" loading="lazy" alt="サンプル画像" width="800" height="600">

フェードイン演出・先読み距離の調整・背景画像への適用など、細かく制御したいときだけ、以下の Intersection Observer 実装を使います。

仕組み:data-srcに本当のURLを退避する

遅延読み込みの基本は、src に最初は本物の画像を入れないことです。本当のURLは data-src に退避しておき、画面に入ったタイミングでJavaScriptが data-srcsrc に移して読み込みを開始します。

HTML:data-srcに本当のURL
<!-- src は軽いプレースホルダー、本物は data-src に -->
<img class="lazy" data-src="photo.jpg"
     src="placeholder.jpg" alt="サンプル画像" width="800" height="600">
width / heightを指定:画像の width / height(または aspect-ratio)を指定しておくと、読み込み前後で高さが変わるレイアウトシフト(CLS)を防げます。

CSSで読み込み後にふわっと表示

読み込みが終わったら .loaded クラスを付け、opacitytransition でふわっと表示します。

CSS
img.lazy {
  width: 100%; height: auto;
  opacity: 0;
  transition: opacity 0.3s ease;
}
img.lazy.loaded {
  opacity: 1; /* 読み込み完了で表示 */
}

JavaScriptで画面に入ったら読み込む

Intersection Observer で画像を監視し、isIntersectingtrue になったら data-srcsrc にセット。読み込めたら .loaded を付け、その画像の監視は unobserve で解除します。

JavaScript:遅延読み込みの実装
document.addEventListener("DOMContentLoaded", () => {
  const lazyImages = document.querySelectorAll("img.lazy");

  const observer = new IntersectionObserver((entries, obs) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      const img = entry.target;
      img.src = img.dataset.src;          // data-src → src で読み込み開始
      img.onload = () => img.classList.add("loaded"); // 完了でフェードイン
      img.onerror = () => img.classList.add("loaded"); // 失敗してもちらつかせない
      obs.unobserve(img);                 // この画像はもう監視しない
    });
  }, {
    rootMargin: "200px", // 画面に入る200px手前で先読み
  });

  lazyImages.forEach((img) => observer.observe(img));
});

Intersection Observer 自体の使い方(オプションや複数監視)はIntersectionObserverで複数要素を監視する方法で詳しく解説しています。

動作イメージ(動くデモ)

↓ ボックスをゆっくりスクロールすると、画面に入ったものだけ「読み込み」が始まります(仕組みを再現したデモ。実際の画像読み込みの代わりに状態を切り替えています):

下にスクロール ↓

画像1(未読み込み)
画像2(未読み込み)
画像3(未読み込み)
画像4(未読み込み)

実際の実装では setTimeout の部分が img.src = img.dataset.src による画像読み込みになります。画面に入ったものだけ順次読み込まれ、初期表示が軽くなるのがポイントです。

rootMarginで「手前から」先読みする

画面に完全に入ってから読み込むと、表示が一瞬遅れて見えることがあります。rootMargin: "200px" のように指定すると、画面に入る200px手前で読み込みを開始でき、スクロール時の見え方が自然になります。

大きすぎると遅延読み込みの効果が薄れ、小さすぎると間に合いません。実際のスクロール速度を見ながら調整しましょう。

背景画像(CSS)を遅延読み込みする

CSSの背景画像は loading="lazy" が使えないため、JavaScriptで遅延読み込みします。data-bg にURLを入れておき、画面に入ったら style.backgroundImage に設定します。

JavaScript:背景画像の遅延読み込み
const el = entry.target;
el.style.backgroundImage = `url(${el.dataset.bg})`;
obs.unobserve(el);

よくある質問(FAQ)

QIntersection Observerで画像の遅延読み込みを実装するには?
Aimg の本当のURLを data-src に入れ、src はプレースホルダーにします。Intersection Observer で画面内に入ったら data-srcsrc に移し、observer.unobserve(img) で監視を解除します。
Qネイティブのloading=”lazy”と何が違いますか?
Aloading="lazy" はHTML属性でブラウザ任せ・JSゼロです。Intersection Observerタイミングや条件を細かく制御でき、フェードインや先読み距離の調整も組み合わせられます。ほとんどの場合は loading="lazy" で十分です(完全ガイド参照)。
Q読み込みに失敗した画像はどうなりますか?
Aimg.onerror で代替画像に差し替えたり、.loaded を付けてちらつきを防いだりします。上の実装でも onerror.loaded を付けています。
Q読み込み後にふわっと表示するには?
A画像に opacity: 0transition を設定し、img.onload.loadedopacity: 1)を付けます。スクロール連動のフェード演出はIntersectionObserverでふわっと表示させる方法も参考になります。

まとめ

Intersection Observer による画像の遅延読み込みは、data-src に本当のURLを退避 → 画面に入ったら src に移す → unobserve で監視解除、という流れで実装します。rootMargin で手前から先読みし、onload でフェードインさせると自然です。

ただし、まずはネイティブの loading="lazy" で足りるかを検討しましょう。ネイティブ属性・LCPへの影響・srcset 対応などをまとめて知りたい場合はLazyload(遅延読み込み)完全ガイドをご覧ください。