【JavaScript】スクロール処理は重い!Intersection ObserverでWebパフォーマンスを劇的に改善する使い方
※本ページのリンクにはプロモーションが含まれています。
こんにちは、Ryohei(@ityryohei)です!
前回の記事では、scrollやresizeといったイベントの連発を制御するためのDebounceとThrottleについて解説しました。しかし、Webパフォーマンスを追求する上では、「スクロールイベントを監視する」こと自体が、そもそもブラウザに負荷をかける根本的な原因となります。
本記事では、高パフォーマンスなWebサイトを作るために欠かせないモダンAPI、Intersection Observerを徹底解説します。これを使えば、Lazy Loadや無限スクロールの実装が、劇的に軽く、シンプルになります。
DebounceやThrottleを使っても、やっぱりスクロールがカクつくことがあるんだ。もっと根本的に、ブラウザに負担をかけずに要素の表示を判定する方法はないのかな?
上記の疑問にお答えします。
では、解説していきます。Intersection Observerとは? スクロール監視の限界をどう超えるか
従来のJavaScriptによるスクロールイベントを使った要素の表示判定(Lazy Loadなど)には、致命的な弱点がありました。
なぜスクロールイベントは重いのか
従来の表示判定は、以下の負荷の高い処理をスクロールのたびにメインスレッドで実行していました。
scrollイベントで処理を起動。getBoundingClientRect()などで要素の正確な座標を計算。- 座標とビューポートを比較して、要素の表示/非表示を判定。
この「座標計算と判定」の処理が、ブラウザのレンダリング(描画)に必要なメインスレッドを占有してしまい、スクロールの滑らかさを損なっていました。
Intersection Observerの優位性
Intersection Observerは、この問題を解決するために設計されたモダンAPIです。
- メインスレッドからの分離: 監視と交差の判定処理をブラウザの内部(メインスレッド外)で実行するため、CPUに負荷をかけません。
- 非同期実行: 処理が完了するのを待つ必要がないため、他の重要な処理や描画を妨害しません。
これにより、従来のイベント監視に比べて、劇的に低負荷で正確な表示判定が可能になります。
基本の使い方!Intersection Observerの3ステップ
Intersection Observerを実装するための手順は非常にシンプルです。
ステップ1:Observer(監視者)の作成
監視処理とオプションを定義し、IntersectionObserverのインスタンスを作成します。
JavaScript
// 監視オプションを定義
const options = {
root: null, // 監視の基準をビューポート(画面全体)に設定
rootMargin: '200px 0px', // ビューポートの200px手前で交差判定を開始(先読み用)
threshold: 0 // ターゲット要素が1ピクセルでも見えたら交差と判定
};
// 交差状態が変わったときに実行されるコールバック関数
const callback = (entries, observer) => {
entries.forEach(entry => {
// entry.isIntersecting が true なら交差した(画面内に入った)
if (entry.isIntersecting) {
// 処理内容を記述
console.log('要素が画面内に入った!', entry.target);
// 重要: 一度処理したら監視を停止する
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(callback, options);
ステップ2:監視オプション (options) の設定
特に重要なオプションは以下の3つです。
| オプション | 意味 | 活用例 |
root | 監視の基準となる要素。nullでビューポート全体。 | 特定のコンテナ内でのみ表示を判定したい場合に、そのコンテナ要素を指定。 |
rootMargin | rootに設定するマージン。CSSのmarginと同じ指定方法。 | Lazy Loadで「ユーザーがスクロールする200px手前で画像を読み始めたい」場合に '200px 0px'などと指定する。 |
threshold | ターゲット要素の何割(0.0~1.0)が見えたときに交差と見なすか。 | 100%見えたらアニメーションを開始したい場合は 1.0、配列で [0, 0.5, 1.0]と指定し、段階的な処理を行うことも可能。 |
ステップ3:ターゲット要素の監視開始
作成したObserverインスタンスのobserve()メソッドに、監視したいDOM要素を渡します。
JavaScript
// 監視したい要素をすべて取得
const targets = document.querySelectorAll('.lazy-load-target');
// 全てのターゲット要素の監視を開始
targets.forEach(target => {
observer.observe(target);
});
実践!Lazy Loadの実装
最も一般的でパフォーマンス改善効果が高い、画像の遅延読み込み (Lazy Load) のフル実装コードを紹介します。
HTML&JavaScript
<html>
<head>
<meta charset="utf-8">
<title>Intersection Observer Lazy Load Demo</title>
<style>
body {
height: 300vh; /* スクロール可能にする */
padding: 50px;
font-family: sans-serif;
}
.lazy-container {
height: 400px;
margin-bottom: 50px;
background-color: #f0f0f0;
border: 1px solid #ccc;
}
.lazy-img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0; /* 初期状態は非表示 */
transition: opacity 0.5s;
}
.lazy-img.is-loaded {
opacity: 1; /* 読み込み完了後表示 */
}
</style>
</head>
<body>
<h1>Intersection Observer Lazy Load デモ</h1>
<p>下にスクロールして、画像が読み込まれるタイミングを確認してください。</p>
<div style="height: 100vh;">スクロールダウン</div>
<div class="lazy-container">
<img class="lazy-img" data-src="https://picsum.photos/id/1018/600/400" alt="Lazy Image 1">
</div>
<div class="lazy-container">
<img class="lazy-img" data-src="https://picsum.photos/id/1025/600/400" alt="Lazy Image 2">
</div>
<div class="lazy-container">
<img class="lazy-img" data-src="https://picsum.photos/id/1040/600/400" alt="Lazy Image 3">
</div>
<script>
// 監視オプション: ビューポートから200px手前で交差と判定
const options = {
root: null,
rootMargin: '200px 0px',
threshold: 0
};
// コールバック関数: 交差時にdata-srcからsrcへURLを移動
const loadImages = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 読み込み開始
img.classList.add('is-loaded');
console.log(`画像をロードしました: ${img.src}`);
observer.unobserve(img); // 監視を終了
}
});
};
const imageObserver = new IntersectionObserver(loadImages, options);
const lazyImages = document.querySelectorAll('.lazy-img');
lazyImages.forEach(img => {
imageObserver.observe(img);
});
</script>
</body>
</html>
実行結果
See the Pen Untitled by ryohei (@intotheprogram) on CodePen.
応用活用!無限スクロールと高度な判定
無限スクロールへの応用
無限スクロールの場合、ページの一番下に「トリガー要素(目印)」を配置し、その要素が画面内に入った瞬間に次のデータを読み込みます。
JavaScript
// トリガー要素が画面内に入ったら、次のコンテンツを非同期で取得する処理
const loadMoreContent = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 重要: コンテンツ読み込み中に複数回発火しないよう、一旦監視を停止
observer.unobserve(entry.target);
fetch('/api/next-page')
.then(response => response.json())
.then(newContent => {
// DOMに新しいコンテンツを追加する処理を実行
// ...
// 読み込み完了後、再度トリガー要素の監視を再開
observer.observe(entry.target);
})
}
});
};
アニメーションに役立つ entry オブジェクトの詳細
コールバック関数に渡される引数entryには、交差判定だけでなく、スクロール連動型のアニメーションに役立つ詳細な情報が含まれています。
| プロパティ名 | 意味 | 活用例 |
intersectionRatio | ターゲット要素とroot要素が交差している割合(0.0~1.0)。 | 要素が画面に入っていく割合に合わせて、透明度やCSSの変形をスムーズに変化させる。 |
boundingClientRect | ターゲット要素自体の正確な位置とサイズ。 | 要素の初期位置やサイズに基づいた複雑なアニメーションの起点として利用。 |
特にintersectionRatioは、従来のスクロールイベント+座標計算では非常に負荷が高かった「要素の表示割合に応じたアニメーション」を、低負荷で実装するために不可欠です。thresholdを複数指定し、intersectionRatioの値を見て処理を分岐させることで、高度な演出が可能になります。
最後に
Intersection Observerは、従来のJavaScriptイベント監視モデルから脱却し、ブラウザの持つ最適化能力を最大限に引き出すための強力なツールです。
- メインスレッドの解放:
スクロール時の座標計算をブラウザに任せ、メインスレッドの負担を大幅に軽減します。 - 低負荷な実装:
Lazy Load、無限スクロール、画面内に入ったときの初回アニメーションなど、実務で必要な処理を低負荷で実現できます。
Webパフォーマンス改善の最前線として、ぜひあなたのWebサイトに導入してみてください。
以上、JavaScriptのIntersection ObserverでWebパフォーマンスを劇的に改善する使い方のご紹介でした!