Intersection Observer 実践ガイド|遅延読み込み・無限スクロール・スクロール検知
Intersection Observer API の使い方を実例で解説。画像遅延読み込み、無限スクロール、要素の可視性検知など実務で使える実装パターンを完全網羅。
スクロール位置に応じた処理は、以前は scroll イベント + getBoundingClientRect() で実装されていました。しかしこの方法はメインスレッドを毎フレーム消費し、パフォーマンスの問題を引き起こします。Intersection Observer API はこれを根本的に解決する、現代的で効率的な代替手段です。
Intersection Observer とは
Intersection Observer は、要素の可視性(ビューポートとの交差)を非同期に監視する API です。
主な特徴:
- メインスレッドをブロックしない(別スレッドで監視)
- 要素がビューポートに入った/出たタイミングでコールバック実行
- 複数要素を一度に監視可能
rootMarginとthresholdで柔軟な制御
ブラウザ対応(2026年4月)
| ブラウザ | サポート開始 |
|---|---|
| Chrome 51 | 2016年5月 |
| Firefox 55 | 2017年8月 |
| Safari 12.1 | 2019年3月 |
| Edge 15 | 2017年4月 |
全ブラウザで完全サポート。フォールバック不要で使用できます。
基本的な使い方
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('要素が表示された:', entry.target);
} else {
console.log('要素が非表示になった:', entry.target);
}
});
});
const target = document.querySelector('.target');
observer.observe(target);
オプションの詳細
const observer = new IntersectionObserver(callback, {
root: null, // 監視コンテナ(null = ビューポート)
rootMargin: '0px', // rootの拡張/縮小
threshold: 0.1, // 交差率(0〜1 または配列)
});
rootMargin
ビューポートを仮想的に拡張/縮小します。CSS の margin 形式で指定します。
// ビューポートの下側を 200px 拡張
// → 下から 200px 手前で検知開始
rootMargin: '0px 0px 200px 0px'
// 上下を 100px 縮小
// → 要素が中央付近に来た時だけ検知
rootMargin: '-100px 0px'
threshold
要素がどれだけ交差したら発火するかを指定します。
// 1% 以上交差したら発火
threshold: 0.01
// 50% 交差したら発火
threshold: 0.5
// 複数のポイントで発火
threshold: [0, 0.25, 0.5, 0.75, 1]
複数指定の場合、それぞれの時点でコールバックが呼ばれます。プログレスバーや段階的アニメーションに便利です。
実践パターン1: 画像の遅延読み込み
ネイティブの loading="lazy" が使えない複雑なケースで有効です。
<img
data-src="/images/hero.jpg"
alt="ヒーロー画像"
class="lazy-image"
/>
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
obs.unobserve(img); // 一度読み込んだら監視解除
}
});
}, {
rootMargin: '200px', // 画面外 200px で読み込み開始
});
document.querySelectorAll('.lazy-image').forEach(img => {
observer.observe(img);
});
.lazy-image {
opacity: 0;
transition: opacity 0.3s;
}
.lazy-image.loaded {
opacity: 1;
}
実践パターン2: 無限スクロール
ページ末尾に達したら次のページをロードします。
<div class="posts">
<!-- 記事リスト -->
</div>
<div id="load-more-trigger"></div>
let page = 1;
let loading = false;
const trigger = document.getElementById('load-more-trigger');
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
loading = true;
try {
const res = await fetch(`/api/posts?page=${page + 1}`);
const data = await res.json();
if (data.posts.length === 0) {
observer.unobserve(trigger);
return;
}
renderPosts(data.posts);
page++;
} finally {
loading = false;
}
}
}, {
rootMargin: '100px',
});
observer.observe(trigger);
実践パターン3: スクロール連動アニメーション
要素が表示されたときにフェードインさせます。
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s, transform 0.6s;
}
.fade-in.is-visible {
opacity: 1;
transform: translateY(0);
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px',
});
document.querySelectorAll('.fade-in').forEach(el => {
observer.observe(el);
});
代替手段: この用途なら、CSS Scroll-Driven Animations の方が JavaScript 不要で簡潔です。
実践パターン4: 目次のアクティブ項目更新
ページスクロールに応じて、目次のアクティブ項目を更新します。
const headings = document.querySelectorAll('article h2, article h3');
const tocLinks = document.querySelectorAll('.toc a');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.id;
const link = document.querySelector(`.toc a[href="#${id}"]`);
if (entry.isIntersecting) {
tocLinks.forEach(l => l.classList.remove('active'));
link?.classList.add('active');
}
});
}, {
rootMargin: '-100px 0px -50% 0px',
threshold: 0,
});
headings.forEach(heading => observer.observe(heading));
実践パターン5: 動画の自動再生/停止
動画が画面内にある時だけ再生します。
const videos = document.querySelectorAll('video');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const video = entry.target;
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
});
}, {
threshold: 0.5, // 50% 表示で再生
});
videos.forEach(video => observer.observe(video));
実践パターン6: アクティブセクションの検知
シングルページのナビゲーションで、現在表示中のセクションをハイライト:
const sections = document.querySelectorAll('section');
const navLinks = document.querySelectorAll('.nav a');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.id;
const link = document.querySelector(`.nav a[href="#${id}"]`);
if (entry.isIntersecting) {
navLinks.forEach(l => l.classList.remove('active'));
link?.classList.add('active');
}
});
}, {
threshold: 0.5,
});
sections.forEach(section => observer.observe(section));
実践パターン7: アナリティクス計測
要素が実際に表示された(=読まれた)ことを計測します。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
const id = entry.target.dataset.trackingId;
// 表示計測を送信
gtag('event', 'view', {
section_id: id,
time: Date.now(),
});
observer.unobserve(entry.target);
}
});
}, {
threshold: [0.5],
});
document.querySelectorAll('[data-tracking-id]').forEach(el => {
observer.observe(el);
});
entry オブジェクトのプロパティ
entry.target // 監視対象の要素
entry.isIntersecting // 交差しているかのbool
entry.intersectionRatio // 交差率(0〜1)
entry.boundingClientRect // 要素の矩形情報
entry.rootBounds // root の矩形情報
entry.time // タイムスタンプ
パフォーマンスと最適化
監視の解除
使い終わった監視は必ず解除しましょう。
// 個別解除
observer.unobserve(element);
// 全解除
observer.disconnect();
複数の Observer を使い分け
用途ごとに Observer を分けると、オプションを最適化できます。
// 画像遅延読み込み用(マージン大)
const lazyLoadObserver = new IntersectionObserver(..., {
rootMargin: '200px',
});
// アニメーション用(ピッタリで発火)
const animationObserver = new IntersectionObserver(..., {
threshold: 0.1,
});
scroll イベントとの比較
| 項目 | scroll イベント | Intersection Observer |
|---|---|---|
| パフォーマンス | 悪い(毎フレーム実行) | 良い(非同期) |
| メインスレッド | ブロック | ブロックしない |
| バッテリー | 消費が多い | 少ない |
| 実装の簡潔さ | 複雑 | シンプル |
| 正確性 | getBoundingClientRect が必要 | 自動計算 |
まとめ
Intersection Observer は、以下のようなケースで特に威力を発揮します。
- 画像・コンポーネントの遅延読み込み: パフォーマンス改善
- 無限スクロール: ユーザー体験の向上
- スクロール連動アニメーション: 滑らかな表示
- アナリティクス: 正確な表示計測
Modern な Web 開発では、スクロール関連の処理はまず Intersection Observer を検討すべきです。scroll イベントを使うのは、本当にそれでなければ実現できない場合だけにしましょう。