ブログ

Lazy Loading画像と動画でパフォーマンス改善

2025.08.18 : Yamamoto Naoki
Coding

Lazy Loading 画像と動画でパフォーマンス改善

はじめに

この記事では「Lazy Loading 画像と動画でパフォーマンス改善」について、初級者から中級者の方にも分かりやすく、実践的な内容をお届けします。 ウェブサイト制作において、このテーマは SEO やユーザー体験の向上に直結するため、ぜひ理解して活用してください。

Lazy Loading(遅延読み込み)は、ビューポート外の画像や動画、iframe などのリソースを、実際に必要になるまで読み込まない最適化手法です。初期表示のリクエスト数と転送量を削減し、First Contentful Paint (FCP) や Time to Interactive (TTI) の改善に役立ちます。

基本概念

Lazy Loading の対象

  • 画像(<img>
  • 動画(<video> のファイル本体、<source>、サムネイル)
  • iframe(地図や埋め込み動画など)
  • 背景画像(CSS 背景は JS での遅延が必要)

実現方法の種類

  • ネイティブ属性loading="lazy"(画像・iframe)、decoding="async"(画像)。実装が簡単で現在は主要ブラウザに広く対応
  • JavaScript + IntersectionObserver: 細かな制御が可能。プレースホルダやアニメーション、フェードインなどの表現に向く

実装手順

1. もっとも簡単: ネイティブ Lazy Loading(画像・iframe)

<!-- 折りたたみ下の画像に lazy を指定 -->
<img
	src="/images/gallery-800.jpg"
	alt="作品ギャラリー"
	width="800"
	height="600"
	loading="lazy"
	decoding="async"
/>

<!-- iframe も同様に対応 -->
<iframe
	src="https://www.youtube.com/embed/xxxx"
	title="紹介動画"
	loading="lazy"
	referrerpolicy="strict-origin-when-cross-origin"
	allowfullscreen
></iframe>

ポイント:

  • width と height を指定して描画領域を確保(CLS 防止)。CSS の aspect-ratio でも可
  • ファーストビュー(LCP 候補)の画像は lazy にしない。必要に応じて fetchpriority="high" を併用

2. ぼかしプレースホルダ(LQIP/Blur-up)

<img
	src="/images/placeholder-blur.jpg"
	data-src="/images/hero-1600.jpg"
	alt="ヒーロー画像"
	class="lazy blur"
	width="1600"
	height="900"
/>
.blur {
	filter: blur(12px);
	transition: filter 300ms ease;
}
.blur.is-loaded {
	filter: blur(0);
}
const lazyTargets = document.querySelectorAll('img.lazy');
const io = new IntersectionObserver(
	(entries, observer) => {
		entries.forEach((entry) => {
			if (!entry.isIntersecting) return;
			const img = entry.target;
			const real = img.getAttribute('data-src');
			if (real) {
				img.src = real;
				img.addEventListener(
					'load',
					() => img.classList.add('is-loaded'),
					{ once: true }
				);
			}
			observer.unobserve(img);
		});
	},
	{ rootMargin: '200px 0px' }
);

lazyTargets.forEach((el) => io.observe(el));

3. IntersectionObserver での汎用実装(data-src/data-srcset

<img
	src="/images/placeholder.jpg"
	data-src="/images/photo-1200.jpg"
	data-srcset="/images/photo-600.jpg 600w, /images/photo-1200.jpg 1200w"
	data-sizes="(max-width: 600px) 600px, 1200px"
	alt="写真"
	width="1200"
	height="800"
	class="js-lazy"
/>
function upgradeSource(target) {
	const src = target.getAttribute('data-src');
	const srcset = target.getAttribute('data-srcset');
	const sizes = target.getAttribute('data-sizes');
	if (src) target.src = src;
	if (srcset) target.srcset = srcset;
	if (sizes) target.sizes = sizes;
}

const observer = new IntersectionObserver(
	(entries, obs) => {
		for (const entry of entries) {
			if (!entry.isIntersecting) continue;
			upgradeSource(entry.target);
			obs.unobserve(entry.target);
		}
	},
	{ rootMargin: '200px 0px', threshold: 0.01 }
);

document
	.querySelectorAll('img.js-lazy')
	.forEach((img) => observer.observe(img));
<noscript>
	<img src="/images/photo-1200.jpg" alt="写真" width="1200" height="800" />
</noscript>

4. 動画の遅延読み込み(preload="none" と遅延アタッチ)

<video
	controls
	preload="none"
	poster="/video/thumb.jpg"
	width="1280"
	height="720"
	class="video-lazy"
>
	<source data-src="/video/movie-1080.mp4" type="video/mp4" />
</video>
const videoObserver = new IntersectionObserver(
	(entries, obs) => {
		entries.forEach((entry) => {
			if (!entry.isIntersecting) return;
			const video = entry.target;
			const source = video.querySelector('source[data-src]');
			if (source && !source.src) {
				source.src = source.dataset.src;
				video.load();
			}
			obs.unobserve(video);
		});
	},
	{ rootMargin: '300px 0px' }
);

document
	.querySelectorAll('video.video-lazy')
	.forEach((v) => videoObserver.observe(v));

5. 背景画像(CSS)の遅延

<div class="card js-bg-lazy" data-bg="/images/card-bg.jpg">コンテンツ...</div>
.card {
	min-height: 240px;
	background-size: cover;
	background-position: center;
}
const bgObserver = new IntersectionObserver((entries, obs) => {
	entries.forEach((entry) => {
		if (!entry.isIntersecting) return;
		const el = entry.target;
		const url = el.getAttribute('data-bg');
		if (url) el.style.backgroundImage = `url("${url}")`;
		obs.unobserve(el);
	});
});

document
	.querySelectorAll('.js-bg-lazy')
	.forEach((el) => bgObserver.observe(el));

ベストプラクティス

  • LCP 候補は lazy にしない: ヒーロー画像やファーストビューのカルーセル 1 枚目は通常読み込みし、fetchpriority="high" を検討
  • CLS 防止width/height または aspect-ratio でレイアウト領域を予約
  • レスポンシブと併用srcset/sizes を合わせて帯域を節約
  • rootMargin のチューニング: 100–300px のプリフェッチ余裕をとる( IntersectionObserver のオプション)
  • ポスター画像の活用(動画)poster を必ず用意し、プレースホルダとして表示
  • iframe の遅延loading="lazy" をデフォルトに。地図や外部ウィジェットに有効
  • アクセシビリティalt を適切に記述。遅延は読み上げ順序に影響しないようマークアップ順序を維持
  • SEO 配慮data-src 手法を用いる場合は <noscript> でフォールバックを提供。ネイティブ lazy(src 明示)ならクローラの理解が安定

フレームワーク/プラットフォームでの活用

  • Next.jsnext/image は既定で最適化。LCP は priority、その他は loading="lazy"(デフォルト)
  • Nuxt/Imagenuxt-img で loading="lazy"sizes を指定
  • WordPress: 5.5+ で loading="lazy" がデフォルト。ヒーローは除外設定を検討
  • ライブラリ: 可能ならネイティブ優先。追加機能が必要なら Lozad.js / lazysizes を選択

テストと検証

  • Lighthouse: 画像要素の遅延読み込み、LCP/CLS の回帰をチェック
  • Chrome DevTools: Network の優先度、Waterfall、img の Decoded Size を確認
  • WebPageTest: 1st/Repeat View、モバイル実機シミュレーションで効果を比較

トラブルシューティング

  1. 画像が表示されない
    • data-src の移し替え忘れ/誤ったセレクタ
    • IntersectionObserver の root がスクロールコンテナに合っていない
    • rootMargin が小さすぎてユーザーより先に読み込めていない
  2. LCP が悪化した
    • LCP 画像に loading="lazy" を付けてしまっている。外し、fetchpriority="high" を付与する
  3. CLS が増えた
    • width/height 未指定。aspect-ratio で領域を設定する
  4. SEO が不安
    • data-src 運用時は <noscript> フォールバックを必ず用意。可能ならネイティブ lazy を使用する

まとめ

Lazy Loading は初期ロードの負荷を下げ、体感速度とエンゲージメントを高める強力な施策です。

  • まずはネイティブ loading="lazy" を適用し、上位要素(LCP 候補)は対象外にする
  • width/height(または aspect-ratio)で CLS を防止
  • 必要に応じて IntersectionObserver でプレースホルダや動画、背景画像まで拡張
  • Lighthouse/DevTools/WebPageTest で計測し、rootMargin や画像サイズを継続的に調整

日々の制作に取り入れて、パフォーマンスと体験の底上げを図りましょう。

アーカイブ

資料ダウンロード 制作依頼・ご相談
イメージ:制作依頼・ご相談
イメージ:制作依頼・ご相談