画像の遅延読み込み実装ガイド - loading=lazy と IntersectionObserver の使い分け
遅延読み込みの基本概念とパフォーマンス効果
画像の遅延読み込み (Lazy Loading) とは、ユーザーのビューポートに画像が近づいたタイミングで初めてダウンロードを開始する技術です。ページ内に 20 枚の画像がある場合、初期表示時にはファーストビューの 2〜3 枚だけを読み込み、残りはスクロールに応じて順次読み込みます。これにより、初期ページロードに必要なネットワークリソースを大幅に削減できます。
パフォーマンスへの効果は劇的です。画像が多いページでは、初期ロード時のネットワークリクエスト数を 70〜80% 削減できます。これにより、ブラウザのネットワーク帯域が HTML、CSS、JavaScript など重要なリソースに集中し、Time to Interactive (TTI) が大幅に改善されます。実測データでは、20 枚の画像を含むページで遅延読み込みを導入した結果、初期ページロード時間が 3.2 秒から 1.4 秒に短縮された事例があります。
ただし、遅延読み込みは万能ではありません。ファーストビュー内の画像に適用すると、逆に LCP が悪化します。ユーザーが最初に目にする画像は即座に読み込む必要があるため、遅延読み込みの対象はファーストビュー外の画像に限定すべきです。この判断を誤ると、Core Web Vitals のスコアが低下し、SEO に悪影響を及ぼします。
ネイティブ loading 属性による実装
HTML の loading 属性は、ブラウザネイティブの遅延読み込み機能を提供します。2024 年時点で主要ブラウザすべてがサポートしており、JavaScript なしで遅延読み込みを実現できる最もシンプルな方法です。
使用方法は極めて簡単です:
<img src="photo.jpg" loading="lazy" alt="写真" width="800" height="600">
loading 属性には 3 つの値があります。lazy はビューポートに近づくまで読み込みを遅延させます。eager は即座に読み込みを開始します (デフォルト動作)。auto はブラウザに判断を委ねます。
ネイティブ遅延読み込みの重要な特性として、ブラウザが「どの程度ビューポートに近づいたら読み込みを開始するか」の閾値を自動的に決定する点があります。Chrome の場合、接続速度に応じて閾値が動的に変化します。高速接続では約 1,250 px 手前、低速接続 (3G 相当) では約 2,500 px 手前から読み込みを開始します。この適応的な動作により、ユーザーがスクロールした際に画像が表示されるまでの待ち時間を最小化しています。
必須の注意点: loading="lazy" を使用する場合、width と height 属性を必ず指定してください。これらがないと、ブラウザは画像のサイズを事前に把握できず、レイアウトシフト (CLS) が発生します。
IntersectionObserver による高度な制御
IntersectionObserver API を使用すると、ネイティブ loading 属性では実現できない細かな制御が可能になります。読み込み開始の閾値、アニメーション効果、プレースホルダーの管理など、UX を細部まで作り込みたい場合に適しています。
基本的な実装パターン:
const observer = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;observer.unobserve(img);}});}, { rootMargin: '200px 0px' });
rootMargin オプションで読み込み開始の閾値を制御できます。'200px 0px' と指定すると、ビューポートの上下 200px 手前に画像が入った時点で読み込みが開始されます。この値を大きくするとスクロール時の画像表示がスムーズになりますが、初期リクエスト数が増加するトレードオフがあります。
threshold オプションを使用すると、画像がどの程度ビューポートに入ったかの割合で発火タイミングを制御できます。threshold: 0.1 は画像の 10% がビューポートに入った時点で発火します。ギャラリーページなどで画像の出現に合わせてフェードインアニメーションを適用する場合に有用です。
プレースホルダー戦略と UX の最適化
遅延読み込みで画像が表示されるまでの間、ユーザーに何を見せるかは UX に大きく影響します。空白のままだとレイアウトが不安定に見え、適切なプレースホルダーを表示することで「画像がここに入る」という期待を伝えられます。
主要なプレースホルダー戦略:
- 固定色プレースホルダー: 画像の支配的な色 (ドミナントカラー) を背景色として設定する方法。ビルド時に画像を解析して色を抽出し、
style="background-color: #3a7bd5"のようにインラインスタイルで適用します - LQIP (Low Quality Image Placeholder): 極小サイズ (20〜40px 幅) の画像をぼかして表示する方法。Base64 エンコードして HTML に直接埋め込むことで、追加のリクエストなしにプレビューを表示できます
- BlurHash: 画像のぼかしプレビューを 20〜30 文字の文字列にエンコードする技術。LQIP より軽量で、Canvas API を使ってデコード・表示します
プレースホルダーから実画像への遷移にはフェードインアニメーションを適用すると、切り替わりが滑らかになります。opacity と transition を組み合わせ、画像の onload イベントで opacity: 1 に変更する実装が一般的です。ただし、アニメーションの duration は 200〜300ms 程度に抑え、ユーザーを待たせている印象を与えないようにします。
LCP への影響と正しい適用範囲の判断
遅延読み込みの最大の落とし穴は、LCP (Largest Contentful Paint) 対象の画像に適用してしまうことです。LCP はファーストビューで最も大きなコンテンツ要素の表示完了時間を測定する指標であり、多くのページではヒーロー画像やメインビジュアルが LCP 要素になります。
LCP 画像に loading="lazy" を設定すると、ブラウザは画像のダウンロードを遅延させるため、LCP スコアが数百ミリ秒〜数秒悪化します。Google の Lighthouse は「LCP 画像に lazy loading が設定されている」という警告を出しますが、実際のフィールドデータ (CrUX) でも明確な悪影響が確認されています。
正しい適用範囲の判断基準:
- 遅延読み込みすべき画像: スクロールしないと見えない画像、記事本文中の画像、フッター付近の画像、サイドバーの画像
- 遅延読み込みすべきでない画像: ヒーロー画像、ファーストビューのロゴ、Above the fold のプロダクト画像、背景画像として使用される大きな画像
LCP 画像には loading="lazy" の代わりに fetchpriority="high" を設定し、さらに <link rel="preload" as="image"> で事前読み込みを指示することで、LCP を最大限に改善できます。この「重要な画像は最優先、それ以外は遅延」という二極化戦略が、Core Web Vitals 最適化の基本です。
フレームワーク別の実装と注意点
モダンなフロントエンドフレームワークは、それぞれ独自の画像最適化コンポーネントを提供しています。これらを活用することで、遅延読み込みの実装を大幅に簡略化できます。
Next.js の next/image コンポーネントは、デフォルトで遅延読み込みが有効です。priority プロパティを true に設定すると遅延読み込みが無効になり、fetchpriority="high" と preload が自動的に適用されます。LCP 画像には必ず priority を設定してください。
React で自前実装する場合、useRef と useEffect で IntersectionObserver を管理するカスタムフックを作成するのが一般的です。コンポーネントのアンマウント時に observer.disconnect() を呼び出してメモリリークを防止することが重要です。
静的サイト (HTML + CSS + JS) では、ネイティブ loading="lazy" を基本とし、プレースホルダーやアニメーションが必要な場合のみ IntersectionObserver を追加する方針が効率的です。ネイティブ属性はブラウザの最適化エンジンと統合されているため、JavaScript 実装よりもパフォーマンスが優れる傾向があります。
いずれのフレームワークでも、画像の width と height (またはアスペクト比) を事前に確定させることが CLS 防止の鍵です。動的に読み込まれる画像のサイズが不明な場合は、API レスポンスに画像のディメンション情報を含めるか、固定のアスペクト比コンテナで囲む設計にします。