Web API 完全ガイド – IntersectionObserver, MutationObserver など
Web API がもたらす パフォーマンス向上
React や Vue などのフレームワークを使っていても、ブラウザネイティブの Web API を活用することで、大きなパフォーマンス向上が実現できます。「スクロール検出に scroll イベントを使っている」「DOM 変更を毎秒チェックしている」といった非効率な実装をしていませんか?
今回は、フロントエンド開発でよく使う Web API、特にIntersectionObserver、MutationObserver、ResizeObserver などを詳しく解説し、実装例を通じて効率的なコード構造を見ていきたいと思います。
対象となる方
- フロントエンド開発でパフォーマンス最適化を考えている方
- Web API の活用方法を知りたい方
- scroll、resize イベントの代替案を探している方
※ このドキュメントはモダンブラウザ対応を想定しています。
IntersectionObserver – 要素の表示検出
IntersectionObserver の基本
IntersectionObserver は、要素がビューポート(画面に見える領域)に入ったか、出たかを効率的に検出します。scroll イベントよりも圧倒的に高速です。
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log(`${entry.target} は表示されました`);
} else {
console.log(`${entry.target} は画面外に出ました`);
}
});
});
// 監視対象を指定
const element = document.getElementById('lazy-image');
observer.observe(element);
IntersectionObserver のオプション
root, rootMargin, threshold を調整して、検出タイミングを制御します。
const observer = new IntersectionObserver(
(entries) => {
// コールバック
},
{
root: null, // viewport を使用
rootMargin: '50px', // viewport より 50px 手前で検出開始
threshold: 0.5, // 要素の 50% が見えたら検出
}
);
実装例:遅延ロード
画像が画面に入ってから実際に読み込むことで、初期ロード時間を削減します。
import { useEffect, useRef } from 'react';
export function LazyImage({ src, alt }: { src: string; alt: string }) {
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && imgRef.current) {
// 画像を読み込む
imgRef.current.src = src;
imgRef.current.alt = alt;
observer.unobserve(imgRef.current);
}
});
});
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [src, alt]);
return <img ref={imgRef} style={{ height: '400px' }} />;
}
実装例:無限スクロール
export function InfiniteScroll() {
const [items, setItems] = useState<string[]>([]);
const [page, setPage] = useState(1);
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// 次ページのデータを取得
fetch(`/api/items?page=${page}`)
.then((r) => r.json())
.then((data) => {
setItems((prev) => [...prev, ...data]);
setPage((p) => p + 1);
});
}
});
if (sentinelRef.current) {
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [page]);
return (
<>
{items.map((item) => <div key={item}>{item}</div>)}
<div ref={sentinelRef} style={{ height: '100px' }} />
</>
);
}
MutationObserver – DOM 変更の監視
MutationObserver の基本
MutationObserver は、DOM 要素の追加・削除・属性変更・テキスト変更などを検出します。
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
console.log('子要素が追加/削除されました');
} else if (mutation.type === 'attributes') {
console.log(`${mutation.attributeName} 属性が変更されました`);
} else if (mutation.type === 'characterData') {
console.log('テキストが変更されました');
}
});
});
const element = document.getElementById('target');
observer.observe(element, {
childList: true, // 子要素の追加・削除を監視
attributes: true, // 属性変更を監視
characterData: true, // テキスト変更を監視
subtree: true, // 子孫要素も監視
});
実装例:リアルタイム通知
export function NotificationCenter() {
const [notifications, setNotifications] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// 新しい通知が追加されたかチェック
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
// element node
const text = (node as HTMLElement).textContent;
if (text) {
setNotifications((prev) => [...prev, text]);
}
}
});
}
});
});
if (containerRef.current) {
observer.observe(containerRef.current, { childList: true });
}
return () => observer.disconnect();
}, []);
return (
<>
<div ref={containerRef} id="notification-container"></div>
<div>
{notifications.map((n, i) => (
<div key={i}>{n}</div>
))}
</div>
</>
);
}
ResizeObserver – リサイズ検出
ResizeObserver の基本
ResizeObserver は、要素のサイズ変更を検出します。window の resize イベントよりも効率的で、特定の要素のサイズ変更に対応します。
export function ResponsiveComponent() {
const [width, setWidth] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
setWidth(entry.contentRect.width);
});
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef}>
<p>Width: {width}px</p>
{width > 600 ? (
<div>Desktop layout</div>
) : (
<div>Mobile layout</div>
)}
</div>
);
}
RequestAnimationFrame – スムーズなアニメーション
RequestAnimationFrame の役割
RequestAnimationFrame(RAF)は、ブラウザの再描画タイミングに合わせてコールバックを実行します。高フレームレートのアニメーションが実現できます。
export function AnimatedCounter() {
const [value, setValue] = useState(0);
const animateTo = (target: number) => {
let current = 0;
const step = target / 30; // 30フレームで到達
const animate = () => {
current += step;
if (current < target) {
setValue(Math.floor(current));
requestAnimationFrame(animate);
} else {
setValue(target);
}
};
requestAnimationFrame(animate);
};
return (
<div>
<p>Value: {value}</p>
<button onClick={() => animateTo(100)}>Animate</button>
</div>
);
}
Fetch API と AbortController
AbortController でリクエストをキャンセル
コンポーネントがアンマウントされるときに、進行中の API リクエストをキャンセルすることが重要です。
export function DataFetcher() {
const [data, setData] = useState<any>(null);
useEffect(() => {
const abortController = new AbortController();
fetch('/api/data', { signal: abortController.signal })
.then((r) => r.json())
.then((data) => {
setData(data);
})
.catch((error) => {
if (error.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
}
});
// クリーンアップ:コンポーネント がアンマウントされるときにキャンセル
return () => abortController.abort();
}, []);
return <div>{JSON.stringify(data)}</div>;
}
Web API の比較表
| API | 用途 | パフォーマンス | ブラウザ対応 |
| IntersectionObserver | 要素の表示検出 | 優秀(scroll より高速) | モダンブラウザ全対応 |
| MutationObserver | DOM 変更検出 | 中(頻繁な変更は注意) | IE11+ |
| ResizeObserver | 要素のリサイズ検出 | 優秀 | 最新ブラウザ |
| RequestAnimationFrame | スムーズなアニメーション | 優秀 | 全ブラウザ対応 |
| Fetch API | 非同期通信 | 標準 | IE 未対応(ポリフィル必要) |
実装例:統合的な使用
遅延ロード + パフォーマンス監視
export function OptimizedImageGallery({
images,
}: {
images: { id: string; src: string }[];
}) {
const [loaded, setLoaded] = useState<Set<string>>(new Set());
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const id = img.dataset.id!;
const image = new Image();
image.src = img.dataset.src!;
image.onload = () => {
img.src = image.src;
setLoaded((prev) => new Set([...prev, id]));
observer.unobserve(img);
};
}
});
},
{ rootMargin: '100px' }
);
document.querySelectorAll('[data-lazy]').forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<div>
{images.map((img) => (
<img
key={img.id}
data-id={img.id}
data-src={img.src}
data-lazy
alt=""
style={{
backgroundColor: loaded.has(img.id) ? 'transparent' : '#e0e0e0',
}}
/>
))}
</div>
);
}
まとめ
以上で Web API 完全ガイドを終えたいと思います。
IntersectionObserver、MutationObserver、ResizeObserver などのモダン Web API を活用することで、scroll イベント依存のコードから解放され、大幅なパフォーマンス向上が実現できます。これらの API はブラウザがネイティブに最適化しているため、JavaScript で自分で実装するよりも圧倒的に効率的です。
ぜひこれらの Web API をプロジェクトに組み込んで、ユーザー体験の向上を目指してください。
