画像のloading=lazyで本文が遅くなる罠: 遅延させない1枚の見分け方
loading=lazyを全画像に付けてLCPが悪化した僕の失敗から、遅延させないヒーロー画像の見分け方、CLS対策、IntersectionObserverとの使い分けを実装で説明します。
「画像、全部 loading="lazy" 付けときました」
数年前の僕です。記事の表示が遅いと言われて、imgタグを片っ端から lazy にした。スコアは上がる気でいました。
結果、トップのヒーロー画像まで遅延がかかって、ページを開いた瞬間に出るはずの一番大きな画像が、一拍おいてから現れるようになったんです。数字で言うと、読者がメインの画像を見られるまでの時間(LCP)が悪化していました。良かれと思ってやったことで、一番見せたい画像を自分で後回しにしていた。
遅延読み込みは「全部に付ける」ものじゃありません。遅延させない1枚を先に決めて、残りを後回しにする技術です。今日はその「遅延させない1枚」の見分け方を中心に、コピペで動くコードと一緒にまとめます。
この記事の要点
- 最初の画面に見える画像(特にLCP候補のヒーロー画像)は遅延しない。
loading="lazy"を付けるのは、スクロールしないと見えない画像だけ。 - どの画像も
widthとheight(またはaspect-ratio)で表示枠を先に確保する。これをサボるとレイアウトがガタつく(CLS)。 - まずはブラウザ標準の
loading="lazy"を使う。display:noneの中、スクロール領域の中、読み込み距離を自分で調整したい、のいずれかが当てはまる時だけ IntersectionObserver を足す。 - ヒーロー画像は
fetchpriority="high"を1枚だけに付ける。全画像に付けると優先順位が壊れて逆効果。 - ぼかしプレースホルダ(blur-up)を入れるなら、本物の
altは必ず残す。装飾扱いにしない。
まず「遅延させない画像」を1枚決める
順番が大事です。lazyを足すより先に、絶対に遅延させない画像を決めます。
それがLCP画像です。LCPはLargest Contentful Paintの略で、ページの中で一番大きく表示されるコンテンツが見えるまでの時間を指します。記事ならアイキャッチ、商品ページならメインビジュアル、ランディングページなら一番上のキービジュアルが、たいていこれにあたります。
web.devのブラウザ標準のLazy Loading解説でも、最初の画面に入る画像、とくにLCP画像は遅延するなとはっきり書かれています。ChromeのLCP discoveryも同じで、LCP画像は「HTMLからすぐ見つかる」「fetchpriority="high" で優先する」「loading=lazy を避ける」の3点を挙げています。
場所ごとの方針を表にすると、こうなります。
| 場所 | loading | fetchpriority | decoding | 必須の対策 |
|---|---|---|---|---|
| ヒーロー画像、LCP候補 | eager または省略 | high を1枚だけ | sync または async | HTMLからすぐ見つかる形にする |
| 記事中盤の図解 | lazy | auto | async | width と height を入れる |
| 商品一覧の2画面目以降 | lazy | auto | async | srcset と sizes で小さい画像を選ばせる |
| カルーセル、無限スクロール | 状況次第 | auto | async | IntersectionObserver で少し手前から読む |
初心者がいちばん引っかかるのは、昔の僕と同じ「重い画像だからヒーローも遅延しよう」という発想です。逆なんですね。重くて大きくて最初に見える画像こそ、優先して取りに行かせる。遅延は「まだ見えていないもの」に対してだけ使う。ここを取り違えると、頑張るほど体感が遅くなります。
そのまま貼れるHTML
フレームワークなしで動く形から見せます。1枚目はファーストビューのヒーロー画像なので loading="eager" と fetchpriority="high"。2枚目は記事下部や一覧にある画像なので loading="lazy" です。
<!-- ファーストビューのヒーロー画像: 遅延させず優先で取りに行く -->
<img
class="hero-image"
src="/images/hero/product-dashboard-1200.webp"
srcset="
/images/hero/product-dashboard-640.webp 640w,
/images/hero/product-dashboard-1200.webp 1200w
"
sizes="100vw"
alt="商品ダッシュボードのファーストビュー"
width="1200"
height="675"
loading="eager"
fetchpriority="high"
decoding="sync"
/>
<!-- 記事下部や一覧の画像: スクロールで見えるまで後回し -->
<img
class="article-image"
src="/images/articles/setup-step-800.webp"
srcset="
/images/articles/setup-step-400.webp 400w,
/images/articles/setup-step-800.webp 800w
"
sizes="(max-width: 720px) 100vw, 720px"
alt="設定手順のスクリーンショット"
width="800"
height="450"
loading="lazy"
decoding="async"
/>
decoding="async" は、画像のデコード(圧縮データを画面に出せる形へ展開する処理)を他の描画と並行しやすくするヒントです。いつも劇的に速くなる魔法ではありませんが、記事中の画像や一覧サムネイルでは自然な初期値です。ヒーロー画像は sync にするか、実測して async のまま残すかを選びます。属性を雰囲気で選ばず、後で出てくる計測でLCPと見た目の安定を確かめる、これが大事です。
srcset と sizes も忘れないでください。srcset は「この画像には640px幅版と1200px幅版があるよ」とブラウザに候補を伝えるもの、sizes は「実際に表示される幅はこれくらい」と教えるものです。スマホで1200px版を読みに行く無駄が、この2つで消えます。
CLSを防ぐCSS
LCPの次に効くのがCLS対策です。CLSはCumulative Layout Shiftの略で、読み込み中に画面のレイアウトがどれだけズレたかを表す数字です。
画像が後から高さを持つと、本文やボタンが下にずれます。読者はタップしようとしたボタンを押し損ねるし、購入導線の位置も動く。地味ですが、これは収益に直結する不具合です。
.image-frame {
/* 画像が来る前から枠の比率を確保しておく */
aspect-ratio: 16 / 9;
background: #f3f4f6;
overflow: hidden;
}
.image-frame > img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 動きを嫌う設定の人には、ふわっと表示を無効化する */
@media (prefers-reduced-motion: no-preference) {
.image-frame > img {
transition: opacity 180ms ease-out;
}
}
img に width と height 属性があると、ブラウザは画像ファイルを取る前から縦横比を計算して、その分の場所を空けておけます。CMSから画像サイズが取れるなら、必ず保存してテンプレートへ渡してください。どうしてもサイズが分からない外部画像は、外側の枠に aspect-ratio を置き、中の画像を object-fit で収めるのが現実的です。
ぼかしプレースホルダ(blur-up)を足すなら
「読み込み中に真っ白だと不安」という時は、blur-upが効きます。小さくぼかした仮画像を先に出して、本物が来たら差し替える手法です。
ここで僕がやらかしたのは、ぼかし画像を背景CSSだけで作って、本物の img の alt を空にしてしまったこと。見た目はそれっぽいんですが、検索エンジンにもスクリーンリーダーにも画像の中身が伝わりません。プレースホルダは見た目の話、alt は中身の話。別物なので両方やります。
<div class="blur-frame" style="aspect-ratio: 16 / 9;">
<img
class="blur-up"
src="/images/reports/report-2026-1200.webp"
alt="2026年の月次レポート画面"
width="1200"
height="675"
loading="lazy"
decoding="async"
style="background-image: url('/images/reports/report-2026-blur.webp'); background-size: cover;"
onload="this.style.backgroundImage='none'"
/>
</div>
背景に極小のぼかし版を敷き、本物が onload で読み終わったら背景を消すだけ。20px幅くらいまで縮めた画像をbase64でCSSに埋め込む方法もありますが、まずはこの形で十分です。プレースホルダが本物より目立って読者を誤誘導する、というのも実際にある失敗なので、ぼかしは控えめにします。
Reactで共通部品にする
同じ判断を毎回手で書くと、必ずどこかで「ヒーローを lazy にする」事故が起きます。Claude CodeにReact実装を頼む時も同じで、ルールを部品の中に閉じ込めておくと崩れにくい。下のラッパーは priority を渡したかどうかで、loading・fetchpriority・decodingをまとめて切り替えます。Next.js専用ではなく、素のReactで動く img ラッパーです。
type SmartImageProps = {
src: string;
srcSet?: string;
sizes?: string;
alt: string;
width: number;
height: number;
priority?: boolean; // ヒーロー/LCP候補のときだけ true
className?: string;
};
export function SmartImage({
src,
srcSet,
sizes,
alt,
width,
height,
priority = false,
className,
}: SmartImageProps) {
// priority のときだけ「遅延しない・優先する・同期デコード」に切り替える
const loading = priority ? "eager" : "lazy";
const fetchPriority = priority ? "high" : "auto";
const decoding = priority ? "sync" : "async";
return (
<span
className={`image-frame ${className ?? ""}`}
style={{ aspectRatio: `${width} / ${height}` }}
>
<img
src={src}
srcSet={srcSet}
sizes={sizes}
alt={alt}
width={width}
height={height}
loading={loading}
fetchPriority={fetchPriority}
decoding={decoding}
/>
</span>
);
}
これなら、商品詳細のメイン画像だけ priority、レビュー画像や関連記事画像は通常のlazy、という運用にできます。Claude Codeに頼む時は「すべての画像をこの部品へ置き換えて」ではなく、「ファーストビュー画像は priority、それ以外だけ移行して」と範囲を切ると失敗しにくいです。ギャラリー全体をこの方針で組む話は画像ギャラリーの遅延読み込みとライトボックスに分けて書いたので、一覧ページを作る人はそちらも見てください。
効く場面を4つ
1. ECの商品一覧
最初の8商品はファーストビューに入りうるので、無理にlazyへ寄せません。9商品目以降、レビュー画像、関連商品、閲覧履歴は loading="lazy" で十分。失敗例は、全商品画像に fetchpriority="high" を付けること。ブラウザの優先順位判断が壊れて、CSSやフォントの取得が後回しになります。優先は譲り合い。全員が「自分が一番」と言ったら、結局誰も優先されません。
2. メディア記事・手順記事
冒頭のアイキャッチはLCP候補なので eager、本文中のスクショや図解は lazy。コード例が多い記事ほど、画像が本文を下に押し下げないよう width と height を必ず入れます。
3. SaaSダッシュボード ユーザーアイコン、顧客ロゴ、レポートのサムネイルは大量に並びます。見えていない行をlazyにすると効きます。ただし上部の主要グラフや、オンボーディングのCTAに関わる画像を遅らせると、体感速度ではなく操作開始が遅れる。お金につながるCTA付近は、速度だけでなく押しやすさも一緒に確認します。計測の設計はアナリティクス実装につなげると運用が楽です。
4. ギャラリー・横スクロールのカルーセル
ここはネイティブlazyだけで足りる場合もありますが、スライドが display: none になっている、スクロール領域の中にある、読み込み距離を自分で決めたい、という条件ではIntersectionObserverの出番です。
IntersectionObserverを使うのはどんな時か
先に結論を言うと、まずはブラウザ標準の loading="lazy" で十分です。Chromeは接続速度に応じて、表示の手前およそ1250px(4Gなど速い回線)から2500px(3Gなど遅い回線)の地点で読み込みを始めます。web.devのデータでは、4Gで遅延画像の97.5%が「見えてから10ミリ秒以内」に表示し終えていました。標準でここまで賢いんです。
それでもIntersectionObserverに切り替える理由は、主に次の3つです。
- 画像を表示直前に差し替えたい(
data-srcから本物のsrcへ) - 読み込みを始める距離を自分で決めたい
- 読み込み後にクラスを外す、別の処理を走らせる、など副作用を足したい
下が、その実装です。HTML側は data-src に本物のURLを逃がし、JS側で監視します。
<img
class="js-lazy-image"
src="/images/placeholders/report-thumb.svg"
data-src="/images/reports/report-2026.webp"
data-srcset="/images/reports/report-2026.webp 1x"
alt="月次レポートのサムネイル"
width="640"
height="360"
/>
const lazyImages = document.querySelectorAll("img[data-src]");
// data-src の中身を本物の src/srcset に移し替える
function loadImage(img) {
img.src = img.dataset.src;
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
img.removeAttribute("data-src");
img.removeAttribute("data-srcset");
}
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((entries, currentObserver) => {
entries.forEach((entry) => {
// まだ画面に近づいていない画像は何もしない
if (!entry.isIntersecting) return;
loadImage(entry.target);
// 一度読んだら監視を外す(無駄な発火を止める)
currentObserver.unobserve(entry.target);
});
}, {
// 画面の300px手前で読み始める。標準より「遅め」に寄せた設定
rootMargin: "300px 0px",
threshold: 0.01,
});
lazyImages.forEach((image) => observer.observe(image));
} else {
// 古いブラウザでは全部すぐ読む(画像が消えるよりマシ)
lazyImages.forEach(loadImage);
}
ここで一つ注意があります。rootMargin: "300px 0px" は、標準の1250pxよりずっと手前、つまりより遅く読み始める設定です。これは「速くしている」のではなく「読み込みを絞っている」ので、スクロールが速い読者には画像の出が一瞬遅れて見えることがあります。手前に余裕を持たせたいなら600px〜1000pxくらいに広げる。ここは実機で触って決める部分です。
それから data-src へ逃がした画像は、JavaScriptが失敗すると読み込まれません。SEO上の主要画像や、本文の理解に必須の図解には、まず普通の src を使ってください。IntersectionObserverは必要な場面に絞るのが安全です。詳細はMDNのIntersection Observer APIが一次情報として正確です。
計測しないと正解は分からない
実装したら、見た目の速さではなく数字で確かめます。Core Web Vitalsの目安は、LCPが2.5秒以内、INP(操作への反応の速さ)が200ミリ秒以内、CLSが0.1以下です。画像の遅延読み込みでは、とくにLCPとCLSを見ます。
npm install web-vitals
import { onCLS, onINP, onLCP } from "web-vitals";
// LCP: 一番大きく表示された要素を取り出して確認する
onLCP((metric) => {
const lastEntry = metric.entries.at(-1);
console.log("LCP", metric.value, lastEntry?.element);
});
onCLS((metric) => {
console.log("CLS", metric.value, metric.entries);
});
onINP((metric) => {
console.log("INP", metric.value, metric.entries);
});
ローカルではChrome DevToolsのPerformanceパネルで、LCP要素がちゃんとヒーロー画像になっているか、その画像リクエストがHTMLから早く見つかっているか、レイアウトのズレがどの要素で起きているかを見ます。onLCP が返す element を見れば、自分が思っている画像と実際のLCP要素がズレていないかすぐ分かります。公開後はPageSpeed InsightsやSearch Consoleで、スマホの実ユーザーの傾向を確認する。Lighthouseだけで判断しないのも大事で、ラボでは速くても、実回線では別の画像がボトルネックになることがあります。サイト全体の速度を腰を据えて直すならパフォーマンス最適化に手順をまとめてあります。
Claude Codeに頼むときの安全な依頼文
画像の遅延読み込みは、Claude Codeが一括置換しやすい領域です。だからこそ、範囲・禁止事項・検証を先に書いて渡します。下のような依頼なら、ヒーロー画像を誤って lazy にする事故が減ります。
{
"goal": "画像の遅延読み込みを安全に追加する",
"scope": [
"記事本文と商品一覧の画像だけを対象にする",
"ファーストビューとLCP候補はlazyにしない"
],
"rules": [
"すべてのimgにalt、width、heightを残す",
"下部画像にはloading=\"lazy\"とdecoding=\"async\"を使う",
"ヒーロー画像にはloading=\"eager\"または省略を使う",
"fetchpriority=\"high\"はLCP候補1枚までにする"
],
"verification": [
"コードフェンスとMDX構文を確認する",
"Chrome DevToolsでLCP要素とCLSを確認する",
"モバイル幅で画像、CTA、本文が重ならないか見る"
]
}
追加の指示では「変更前後の差分」「lazyにしなかった画像とその理由」「測定で見る指標」を報告させます。実装だけ頼むより、判断のログを残す方が次の改善に効きます。
ありがちな失敗チェックリスト
- ヒーロー画像、商品詳細のメイン画像、上部CTAの背景画像をlazyにしていないか。
widthとheightを消してCLSを増やしていないか。- すべての画像に
fetchpriority="high"を付けていないか。 srcsetの候補画像が同じ縦横比になっているか。- CSSの背景画像に
loading属性を付けようとしていないか(背景画像には効きません)。 altを空にして、意味のある図解を装飾扱いにしていないか。- JavaScript無効時に、重要画像が完全に消えないか。
- プレースホルダが本物より目立って、読者を誤誘導していないか。
よくある質問
Q. loading="lazy" を全部の画像に付けたらダメなんですか?
A. ファーストビューに見える画像、とくにLCP画像には付けないでください。最初に見える画像を遅延させると、一番大きなコンテンツが出るまでの時間が延びて、体感がはっきり遅くなります。lazyは「スクロールしないと見えない画像」専用です。
Q. loading="lazy" と IntersectionObserver、どっちを使えばいいですか?
A. まず loading="lazy" です。標準だけで大半は足ります。display:none の中、スクロール領域の中、読み込み距離を自分で調整したい、本物に差し替える前に処理を挟みたい、のいずれかが当てはまる時だけ IntersectionObserver に切り替えます。
Q. fetchpriority="high" は何枚まで付けていいですか?
A. 実質1枚です。LCP候補のヒーロー画像だけに付けます。全画像に付けると優先順位が横並びになり、かえってCSSやフォントの読み込みを邪魔します。
Q. width と height は CSS で指定するからHTML属性は不要では?
A. HTML属性として残してください。属性があると、ブラウザは画像ファイルを取る前から縦横比を計算して場所を空けられます。これがCLS(レイアウトのズレ)を防ぎます。CSSの aspect-ratio でも代用できますが、両方あると安心です。
Q. blur-upのプレースホルダを入れたら alt は要らないですよね?
A. 必要です。プレースホルダは見た目の話、alt は中身の話で別物です。alt を空にすると、検索エンジンとスクリーンリーダーに画像の意味が伝わりません。装飾画像でない限り、必ず書きます。
実際に試した結果
冒頭の「全部lazy」事故のあと、僕がやり方を変えて一番効いたのは、lazyを足すことではなく、lazyにしない画像を先に決めることでした。ヒーロー画像を eager、本文下部のスクショを lazy、全画像に寸法指定、srcset でスマホ画像を分岐。この順番にしただけで、LCPとCLSを悪化させずに転送量を下げられました。onLCP でLCP要素を毎回確認する癖をつけてからは、「思っていた画像と違う」という勘違いも消えました。
Claude Codeに任せる時も同じで、最初に失敗例と検証項目を渡しておくと、あとでヒーロー画像を eager に戻す手戻りが減ります。画像周りの依頼をまとめて固めたい人は無料チートシートから始めて、テンプレートやレビュー観点が欲しくなったら教材一覧へ、チームで速度・SEO・CTAまで一気に直すなら研修・導入相談へ、という流れにしています。遅延読み込みは、スコアのための作業じゃなくて、読者が一番見たい画像を最短で届けるための交通整理です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。