はじめに:なぜ2026年に画像最適化がこれほど重要なのか
突然ですが、あなたのサイトのページ容量、画像がどれくらい占めているか把握していますか? HTTP Archiveの最新データによると、Webページの総容量のうち50%以上を画像が占めているのが現実です。中央値で約1.8MBもの画像データがページごとに転送されています。モバイル回線でこの量を読み込ませるのは、正直かなりキツい。
そして2026年の今、GoogleはCore Web Vitalsをランキングシグナルとしてがっつり使っています。画像はその中でも特にLCP(Largest Contentful Paint)とCLS(Cumulative Layout Shift)の2つに直接影響する最大の要因です。ヒーロー画像の最適化が甘いだけで、LCPが数秒も遅くなることは珍しくありません。
「画像を最適化しましょう」――もう何年も言われてきたことですよね。でも2026年には、AVIF形式の普及、fetchpriority属性の標準化、画像CDNの進化など、使える武器がかなり増えました。逆に言うと、これらを活用しないのは本当にもったいない。
この記事では、LCPを中心に画像パフォーマンスを改善するための実践テクニックを網羅的に解説していきます。コード例もたっぷり載せているので、そのまま自分のプロジェクトに持っていけるはずです。かなりのボリュームですが、必要な章だけ拾い読みしてもらっても全然OKですよ。
第1章:LCPと画像の関係を正しく理解する
LCPとは何か
LCP(Largest Contentful Paint)は、ビューポート内で最も大きな可視要素がレンダリングされるまでの時間を測定する指標です。名前の通り「最大のコンテンツが描画されるまで」の時間なんですが、実際のWebサイトでは約70%のケースでLCP要素が画像だという調査結果があります。
つまり、LCPを改善したいなら、まず画像を攻めるのが最も効果的。これはもう間違いありません。
Googleが定めるLCPのしきい値はこうなっています:
- 良好(Good):2.5秒以下
- 改善が必要(Needs Improvement):2.5秒〜4.0秒
- 不良(Poor):4.0秒以上
目標は当然2.5秒以下。でも実際にこれを達成しているサイトは全体の半分程度にとどまっていて、多くのサイトが画像の最適化不足でこのラインを超えてしまっています。自分もクライアントサイトの監査で何度もこの問題に遭遇してきました。
LCP画像の読み込みプロセス
LCP画像がレンダリングされるまでには、4つのフェーズがあります:
- TTFB(Time to First Byte):サーバーから最初のバイトが届くまでの時間
- Resource Load Delay:ブラウザがHTMLを解析して画像のリクエストを開始するまでの遅延
- Resource Load Duration:画像のダウンロードにかかる時間
- Element Render Delay:画像がダウンロード完了してから実際にレンダリングされるまでの時間
画像の最適化は主に2番目と3番目のフェーズに効きます。フォーマットの最適化でファイルサイズを減らし(3番目)、fetchpriorityやプリロードで読み込み開始を早める(2番目)。この2つをうまく組み合わせることで、劇的なLCP改善が実現できるんです。
LCPの測定方法
さて、まずは自分のサイトのLCPを正確に測ることから始めましょう。PerformanceObserver APIを使えば、実際のユーザー環境でのLCPをリアルタイムに計測できます。
// PerformanceObserver API を使ったLCP測定
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
// 最後のエントリが最終的なLCP値
const lastEntry = entries[entries.length - 1];
console.log('LCP value:', lastEntry.startTime.toFixed(2), 'ms');
console.log('LCP element:', lastEntry.element);
console.log('LCP element tag:', lastEntry.element?.tagName);
console.log('LCP URL:', lastEntry.url); // 画像の場合はそのURL
// LCPの評価
if (lastEntry.startTime <= 2500) {
console.log('✅ Good LCP');
} else if (lastEntry.startTime <= 4000) {
console.log('⚠️ Needs Improvement');
} else {
console.log('❌ Poor LCP');
}
// 分析データをサーバーに送信
navigator.sendBeacon('/analytics/lcp', JSON.stringify({
value: lastEntry.startTime,
element: lastEntry.element?.tagName,
url: lastEntry.url,
size: lastEntry.size,
timestamp: Date.now()
}));
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
このコードを本番サイトに埋め込んでおけば、実ユーザーのLCPデータが集められます。Lighthouseだけだとラボ環境の値しかわからないので、リアルユーザーモニタリング(RUM)と組み合わせて使うのが鉄板ですね。lastEntry.elementを見れば、何がLCP要素になっているかも特定できます。
第2章:モダン画像フォーマット — AVIF vs WebP vs JPEG
AVIF:2026年の本命フォーマット
AVIF(AV1 Image File Format)は、AV1ビデオコーデックをベースにした次世代画像フォーマットです。2026年現在、ブラウザサポート率は約92%に達しており、もはや実験的な技術ではありません。
AVIFの圧縮性能は正直かなり驚異的です:
- JPEGと比較して約50%のファイルサイズ削減(同等の画質で)
- WebPと比較して約20〜30%のファイルサイズ削減
- HDR(ハイダイナミックレンジ)と広色域をサポート
- アルファチャンネル(透過)対応(PNGの代替としても使える)
- アニメーション対応(GIFの代替としても使える)
ただし、デメリットもあります。AVIFはエンコード速度がWebPやJPEGに比べてかなり遅い。大量の画像を変換するにはそれなりのマシンパワーと時間が必要です。それと、プログレッシブ表示(段階的に解像度が上がる表示)には対応していないので、大きな画像では読み込み中に「パッ」と表示される感じになります。このあたりは好みが分かれるところかもしれません。
WebP:今でも最強の汎用フォーマット
WebPはGoogleが開発した画像フォーマットで、2026年時点ではほぼ全てのブラウザでサポートされています。JPEGと比較して約25〜35%のファイルサイズ削減を実現し、エンコード速度も高速。
WebPの強みは何と言っても安定性と信頼性です:
- ブラウザサポート率が事実上100%
- lossy/losslessの両方に対応
- アルファチャンネル対応
- アニメーション対応
- エンコード・デコードが高速
個人的には、「迷ったらまずWebP」というのが一番安全な選択だと思っています。
いつどのフォーマットを使うべきか
結論から言うと、2026年の最適戦略は「AVIFファースト、WebPフォールバック、JPEGラストリゾート」です。具体的には:
- ヒーロー画像やLCP画像:AVIF最優先(サイズ削減がLCPに直結するため)
- 一般的な記事画像:WebPがコストパフォーマンス最高
- 透過が必要な画像:AVIFまたはWebP(PNGからの移行で大幅削減)
- アニメーション:AVIF > WebP > GIF(GIFは完全に非推奨)
<picture>要素でフォーマットフォールバックを実装
<picture>要素を使えば、ブラウザが対応する最適なフォーマットを自動的に選択してくれます。これがフォーマット戦略の基本中の基本。
<!-- AVIF → WebP → JPEG のフォールバックチェーン -->
<picture>
<!-- 最優先:AVIF(最高圧縮率) -->
<source
srcset="hero-image.avif"
type="image/avif">
<!-- 次点:WebP(広いサポート) -->
<source
srcset="hero-image.webp"
type="image/webp">
<!-- フォールバック:JPEG(全ブラウザ対応) -->
<img
src="hero-image.jpg"
alt="メインビジュアル:サービスの特徴を示すイメージ"
width="1200"
height="630"
loading="eager"
fetchpriority="high"
decoding="async">
</picture>
ポイントをいくつか挙げておきます:
<source>の順番は圧縮率の高い順(AVIF → WebP)にするwidth、height、loading、fetchpriorityは<img>要素に指定する- LCP画像には必ず
loading="eager"とfetchpriority="high"を付ける(これ超大事) alt属性はアクセシビリティのためにしっかり記述する
ヒーロー画像をJPEG(150KB)からAVIF(約75KB)に切り替えるだけで、LCPが200〜500ms改善した事例は数多く報告されています。たったこれだけの変更で、「改善が必要」から「良好」に格上げされることもあるんですよ。
第3章:レスポンシブ画像 — srcsetとsizesの完全攻略
なぜレスポンシブ画像が必要なのか
デスクトップ向けに1600pxの画像を用意して、それを375pxのスマートフォンにそのまま送る。これ、帯域幅の無駄遣いだけじゃなく、LCPにも悪影響です。スマートフォンは通常、デスクトップよりネットワーク速度が遅いのに、不必要に大きなファイルをダウンロードさせてしまうわけですから。
レスポンシブ画像の仕組みを使えば、デバイスの画面サイズとDPR(Device Pixel Ratio)に応じて最適なサイズの画像をブラウザに自動選択させることができます。
srcsetのwidth descriptor
srcset属性ではw記述子(width descriptor)を使って、各画像の実際の幅をピクセルで指定します。ブラウザはこの情報とsizes属性を組み合わせて、最適な画像を選びます。
<img
srcset="
article-photo-400.webp 400w,
article-photo-800.webp 800w,
article-photo-1200.webp 1200w,
article-photo-1600.webp 1600w,
article-photo-2000.webp 2000w"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 75vw,
(max-width: 1280px) 50vw,
800px"
src="article-photo-800.webp"
alt="記事のメイン画像"
width="1600"
height="900"
loading="lazy"
decoding="async">
このコードの動作を分解するとこうなります:
sizesがビューポートの幅に応じて「この画像は画面の何%の幅で表示される」を宣言- ブラウザはその情報とデバイスのDPRを掛け合わせて、必要なピクセル数を計算
srcsetの中から最適な画像を選んでダウンロード
たとえば、幅375pxのiPhone(DPR 3x)の場合:100vw × 3 = 1125pxが必要なので、1200wの画像が選ばれます。一方、幅1440pxのデスクトップ(DPR 1x)で50vw = 720pxなら、800wの画像で十分。この違いだけでも、転送量にかなりの差が出るんですよね。
sizes属性のベストプラクティス
sizes属性は「この画像がレイアウト上どのくらいの幅で表示されるか」をブラウザに伝えるためのものです。CSSのメディアクエリと似た書き方で、ブレークポイントごとの表示幅を指定します。
よくある間違いは、sizesを省略すること。省略するとブラウザはデフォルトで100vw(ビューポート全幅)と仮定するため、サイドバーがあるレイアウトなどでは不必要に大きな画像がダウンロードされてしまいます。これ、意外とやりがちなので気をつけてください。
Art Directionで画像の構図を変える
レスポンシブ画像には、サイズの最適化だけでなくArt Direction(アートディレクション)という使い方もあります。これは画面サイズに応じて、まったく別の構図やクロップの画像を表示させるテクニックです。
<!-- アートディレクション:デバイスに応じて最適な構図を提供 -->
<picture>
<!-- モバイル:縦長のクロップ -->
<source
media="(max-width: 767px)"
srcset="hero-mobile-400.avif 400w,
hero-mobile-800.avif 800w"
sizes="100vw"
type="image/avif">
<source
media="(max-width: 767px)"
srcset="hero-mobile-400.webp 400w,
hero-mobile-800.webp 800w"
sizes="100vw"
type="image/webp">
<!-- タブレット:中程度のクロップ -->
<source
media="(max-width: 1023px)"
srcset="hero-tablet-800.avif 800w,
hero-tablet-1200.avif 1200w"
sizes="100vw"
type="image/avif">
<source
media="(max-width: 1023px)"
srcset="hero-tablet-800.webp 800w,
hero-tablet-1200.webp 1200w"
sizes="100vw"
type="image/webp">
<!-- デスクトップ:フル幅のワイド画像 -->
<source
srcset="hero-desktop-1200.avif 1200w,
hero-desktop-1600.avif 1600w,
hero-desktop-2000.avif 2000w"
sizes="100vw"
type="image/avif">
<source
srcset="hero-desktop-1200.webp 1200w,
hero-desktop-1600.webp 1600w,
hero-desktop-2000.webp 2000w"
sizes="100vw"
type="image/webp">
<img
src="hero-desktop-1200.jpg"
alt="ブランドのヒーローイメージ"
width="2000"
height="800"
fetchpriority="high"
decoding="async">
</picture>
ちょっと複雑に見えますよね。でもやっていることはシンプルです。media属性でブレークポイントごとに異なる画像セットを指定し、さらにその中でtype属性によるフォーマットフォールバックも実現しています。スマホでは人物を大きくクロップした縦長画像、デスクトップでは全体が見えるワイド画像、といった使い分けが可能になります。
第4章:fetchpriorityとpreloadでLCP画像を最速で届ける
fetchpriority="high"の威力
fetchpriority属性は、リソースの読み込み優先度をブラウザに明示的に伝えるための属性です。2026年現在、主要なブラウザすべてでサポートされています。
Googleが自社サービスのGoogle Flightsでテストした結果、LCP画像にfetchpriority="high"を追加しただけで、LCPが2.6秒から1.9秒に改善しました。コード1行の追加で0.7秒の改善。これを使わない手はないでしょう。
<!-- LCP画像にfetchpriority="high"を指定 -->
<img
src="hero-banner.webp"
alt="サイトのメインバナー"
width="1200"
height="600"
fetchpriority="high"
loading="eager"
decoding="async">
<!-- 重要でない画像はfetchpriority="low"で優先度を下げる -->
<img
src="decorative-icon.webp"
alt=""
width="48"
height="48"
fetchpriority="low"
loading="lazy"
decoding="async">
重要なポイントとして、fetchpriority="high"を付ける画像は1ページに1〜2枚に留めてください。すべての画像に付けてしまうと、優先度の意味がなくなります。LCP要素と特定できる画像にだけ付けるのが正解です。
<link rel="preload">でLCP画像を先読み
通常、ブラウザはHTMLを解析して<img>タグを発見してから初めて画像のリクエストを開始します。でも<link rel="preload">を<head>に配置すれば、HTML解析の初期段階で画像のダウンロードを開始できるんです。
<!-- <head>内に配置:LCP画像のプリロード -->
<link
rel="preload"
as="image"
href="hero-image.webp"
type="image/webp"
fetchpriority="high">
これでHTML解析の非常に早い段階で画像のダウンロードが始まります。CSSの背景画像や、JavaScriptで動的に挿入される画像には特に効果的。
レスポンシブ画像のプリロード
レスポンシブ画像をプリロードする場合は、imagesrcsetとimagesizes属性を使います。これ、意外と知らない人が多いんですが、めちゃくちゃ便利なんですよ。
<!-- レスポンシブ画像のプリロード -->
<link
rel="preload"
as="image"
imagesrcset="
hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w,
hero-1600.avif 1600w"
imagesizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 75vw,
50vw"
type="image/avif"
fetchpriority="high">
<!-- WebPフォールバック用のプリロード -->
<link
rel="preload"
as="image"
imagesrcset="
hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w,
hero-1600.webp 1600w"
imagesizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 75vw,
50vw"
type="image/webp"
fetchpriority="high">
ブラウザはtype属性を見て対応するフォーマットのプリロードだけを実行します。非対応フォーマットは無視されるので、不要なダウンロードが発生する心配はありません。
実装パターンまとめ
LCP画像に対して行うべきことを優先度順にまとめておきます:
fetchpriority="high"を<img>に追加(最も手軽で効果大)loading="eager"を明示的に指定(lazy-loadを絶対に付けない)<link rel="preload">で先読みを追加(特にCSSや背景画像に有効)- フォーマット最適化(AVIF/WebP)でファイルサイズを削減
この4つを全部やるだけでも、体感速度はかなり変わるはずです。
第5章:遅延読み込み(Lazy Loading)の正しい戦略
ネイティブLazy Loading
ブラウザネイティブの遅延読み込みは、loading="lazy"属性を追加するだけで実装できます。JavaScriptは不要。シンプルで強力です。
<!-- ファーストビュー外の画像にlazy loadingを適用 -->
<img
src="article-thumbnail.webp"
alt="記事のサムネイル"
width="400"
height="300"
loading="lazy"
decoding="async">
ブラウザはビューポートに近づいた画像だけをダウンロードし、まだ見えていない画像のリクエストを遅延させます。ページの初期読み込みで必要なデータ量を大幅に削減できるため、特に画像がたくさんある一覧ページやギャラリーページで威力を発揮します。
絶対にやってはいけないこと:LCP画像のLazy Load
ここ、本当に重要なので太字にします。LCP画像には絶対にloading="lazy"を付けてはいけません。
理由はシンプル。loading="lazy"を付けると、ブラウザはその画像のダウンロードを遅延させます。ファーストビューに表示される画像なのに、わざわざ読み込みを遅らせるわけです。これは完全に逆効果で、LCPが数百ミリ秒から1秒以上も悪化することがあります。
Lighthouseでも「LCP画像にlazy loadingが設定されています」という警告が出ますが、実際にこのミスをしているサイトは驚くほど多いんです。WordPressのデフォルト設定や、ライブラリの一括適用で気づかないうちにLCP画像までlazyにしてしまっているケースをよく見かけます。(正直、自分も昔やらかしたことがあります。)
正しい使い分けはこう:
- LCP画像(ヒーロー画像など):
loading="eager"+fetchpriority="high" - ファーストビュー内のその他の画像:
loading="eager"(デフォルト) - ファーストビュー外の画像:
loading="lazy"
Intersection Observer APIによる高度な遅延読み込み
ネイティブのloading="lazy"だけでは制御が足りない場合、Intersection Observer APIを使って細かくカスタマイズできます。たとえば「ビューポートの200px手前に来たらダウンロード開始」といった設定が可能です。
// Intersection Observer による高度な遅延読み込み
class AdvancedLazyLoader {
constructor(options = {}) {
this.rootMargin = options.rootMargin || '200px 0px'; // 200px手前から読み込み開始
this.threshold = options.threshold || 0.01;
this.loadedCount = 0;
this.init();
}
init() {
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{
rootMargin: this.rootMargin,
threshold: this.threshold
}
);
// data-src属性を持つすべての画像を監視対象に
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach((img) => this.observer.observe(img));
console.log(`LazyLoader: ${lazyImages.length}枚の画像を監視中`);
}
handleIntersection(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (srcset) {
img.srcset = srcset;
}
if (src) {
img.src = src;
}
img.removeAttribute('data-src');
img.removeAttribute('data-srcset');
img.classList.add('loaded');
this.loadedCount++;
}
}
// 使用例
document.addEventListener('DOMContentLoaded', () => {
new AdvancedLazyLoader({
rootMargin: '300px 0px' // 300px手前から読み込み
});
});
対応するHTMLはこんな感じ:
<!-- Intersection Observer用のマークアップ -->
<img
data-src="photo-800.webp"
data-srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
sizes="(max-width: 640px) 100vw, 50vw"
alt="遅延読み込みされる画像"
width="800"
height="600"
class="lazy">
decoding属性も忘れずに
decoding="async"は、画像のデコード処理をメインスレッドとは非同期で行うようブラウザにヒントを与えます。これにより、画像のデコードによるレンダリングブロックを防ぎ、ページの応答性が維持されます。
<!-- ほとんどの画像にdecoding="async"を推奨 -->
<img
src="content-image.webp"
alt="コンテンツ画像"
width="800"
height="450"
decoding="async"
loading="lazy">
LCP画像にもdecoding="async"は使って大丈夫。これはダウンロードの優先度には影響せず、デコード処理の挙動だけに関係するものなので。
第6章:画像CDNと自動最適化パイプライン
画像CDNのメリット
画像CDN(Cloudinary、imgix、Cloudflare Imagesなど)を使うと、画像の最適化を完全に自動化できます。手動でAVIFやWebPを生成する必要がなくなるので、運用コストが劇的に下がります。
画像CDNが提供する主な機能はこちら:
- 自動フォーマット変換(Auto-format):ブラウザのAcceptヘッダーを見て、対応する最適なフォーマットを自動選択
- リアルタイムリサイズ:URLパラメータでサイズを指定するだけで、サーバー側で動的にリサイズ
- 自動品質調整:画像の内容に応じて最適な圧縮率を自動適用
- グローバルCDN配信:世界中のエッジサーバーからキャッシュ配信
- レスポンシブ画像の自動生成:1枚の原画から複数サイズを動的に生成
URLベースの画像変換
画像CDNの最大の利点の一つが、URLパラメータだけで画像を変換できること。コードを見てもらうのが一番わかりやすいと思います:
<!-- Cloudinary の例 -->
<img
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/hero.jpg"
alt="自動最適化された画像"
width="800"
height="450">
<!-- 各パラメータの意味 -->
<!-- f_auto : ブラウザに応じた最適フォーマットを自動選択 -->
<!-- q_auto : 画像内容に応じた最適品質を自動設定 -->
<!-- w_800 : 幅800pxにリサイズ -->
<!-- imgix の例 -->
<img
src="https://example.imgix.net/hero.jpg?auto=format,compress&w=800&fit=max"
alt="imgixで最適化された画像"
width="800"
height="450">
<!-- Cloudflare Images の例 -->
<img
src="https://example.com/cdn-cgi/image/format=auto,quality=80,width=800/hero.jpg"
alt="Cloudflare Imagesで最適化された画像"
width="800"
height="450">
f_auto(Cloudinaryの場合)を指定するだけで、ブラウザのAcceptヘッダーに基づいてAVIF対応ブラウザにはAVIFを、WebP対応にはWebPを、それ以外にはJPEGを自動的に返してくれます。<picture>要素によるフォーマットフォールバックをサーバー側でやってくれるイメージですね。超便利。
レスポンシブ画像とCDNの組み合わせ
<!-- 画像CDNとsrcsetの組み合わせ -->
<img
srcset="
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_400/product.jpg 400w,
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/product.jpg 800w,
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1200/product.jpg 1200w,
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1600/product.jpg 1600w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/product.jpg"
alt="商品画像"
width="1600"
height="900"
loading="lazy"
decoding="async">
原画を1枚アップロードするだけで、CDNが複数サイズ×複数フォーマットを動的に生成してくれます。ビルド時に大量の画像バリエーションを生成・管理する手間がゼロになるのは、本当にありがたいですよね。
ビルドツールでの画像最適化(sharp)
画像CDNを使わない場合は、ビルドパイプラインでの最適化が必要です。Node.jsのsharpライブラリは高速で信頼性が高く、プロダクション環境で広く使われています。
// sharp を使った画像最適化スクリプト (optimize-images.mjs)
import sharp from 'sharp';
import { readdir, mkdir } from 'fs/promises';
import path from 'path';
const INPUT_DIR = './src/images';
const OUTPUT_DIR = './dist/images';
const WIDTHS = [400, 800, 1200, 1600, 2000];
// 出力フォーマットの設定
const FORMATS = [
{
name: 'avif',
options: { quality: 50, effort: 4 } // effort: 0-9(高いほど圧縮率向上だが遅い)
},
{
name: 'webp',
options: { quality: 75, effort: 4 }
},
{
name: 'jpeg',
options: { quality: 80, progressive: true, mozjpeg: true }
}
];
async function optimizeImage(inputPath) {
const filename = path.basename(inputPath, path.extname(inputPath));
const image = sharp(inputPath);
const metadata = await image.metadata();
const tasks = [];
for (const width of WIDTHS) {
// 元画像より大きいサイズはスキップ
if (width > metadata.width) continue;
for (const format of FORMATS) {
const outputFilename = `${filename}-${width}.${format.name}`;
const outputPath = path.join(OUTPUT_DIR, outputFilename);
const task = sharp(inputPath)
.resize(width, null, {
withoutEnlargement: true,
fit: 'inside'
})
[format.name](format.options)
.toFile(outputPath)
.then((info) => {
console.log(
`✅ ${outputFilename}: ${(info.size / 1024).toFixed(1)}KB`
);
});
tasks.push(task);
}
}
await Promise.all(tasks);
}
async function main() {
await mkdir(OUTPUT_DIR, { recursive: true });
const files = await readdir(INPUT_DIR);
const imageFiles = files.filter((f) =>
/\.(jpg|jpeg|png|tiff|webp)$/i.test(f)
);
console.log(`${imageFiles.length}枚の画像を最適化中...\n`);
for (const file of imageFiles) {
const inputPath = path.join(INPUT_DIR, file);
console.log(`処理中: ${file}`);
await optimizeImage(inputPath);
console.log('');
}
console.log('すべての画像の最適化が完了しました!');
}
main().catch(console.error);
このスクリプトは、入力ディレクトリの画像を5つの幅×3つのフォーマット=最大15バリエーションに変換します。CI/CDパイプラインに組み込んでおけば、画像が追加されるたびに自動的に最適化されるので楽ちんです。
第7章:CLSを防ぐための画像テクニック
なぜ画像がレイアウトシフトを起こすのか
画像がCLS(Cumulative Layout Shift)を引き起こす典型的なシナリオはこうです。ブラウザがHTMLを解析している段階では画像のサイズがわからないため、幅0×高さ0のスペースしか確保しません。その後、画像がダウンロードされてサイズが判明すると、レイアウトが「ガクッ」とずれる。あの不快な体験がCLSです。
CLSのしきい値は0.1以下が良好。そして朗報なのが、画像が原因のCLSは完全に防げるということ。以下のテクニックを使えば大丈夫です。
width/height属性は必ず設定する
最もシンプルで効果的な対策は、<img>タグにwidthとheight属性を設定することです。モダンブラウザはこれらの属性からアスペクト比を自動的に計算し、画像の読み込み前にスペースを確保してくれます。
<!-- ❌ ダメな例:width/heightがない -->
<img src="photo.webp" alt="写真">
<!-- ✅ 良い例:width/heightを明示 -->
<img
src="photo.webp"
alt="写真"
width="800"
height="600"
loading="lazy"
decoding="async">
CSSでwidth: 100%とheight: autoを指定している場合でも、HTML側のwidth/height属性は必要です。ブラウザはHTML属性からアスペクト比を計算するので。ここ、地味に重要なポイントです。
aspect-ratio CSSプロパティ
CSSのaspect-ratioプロパティを使えば、より柔軟にアスペクト比を制御できます。
<!-- HTMLマークアップ -->
<div class="image-container">
<img
src="hero.webp"
alt="ヒーロー画像"
width="1600"
height="900"
class="responsive-image">
</div>
/* CSS: レスポンシブ画像のスタイル */
.responsive-image {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* アスペクト比を明示 */
object-fit: cover; /* 画像のフィット方法 */
display: block;
}
/* 画像コンテナのスタイル */
.image-container {
width: 100%;
max-width: 1200px;
contain: layout style; /* ブラウザにレイアウト計算のヒントを与える */
overflow: hidden;
}
/* プレースホルダー付きの画像コンテナ */
.image-container--with-placeholder {
position: relative;
background-color: #f0f0f0; /* 読み込み中のプレースホルダー色 */
aspect-ratio: 16 / 9;
}
.image-container--with-placeholder img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s ease;
}
.image-container--with-placeholder img.loaded {
opacity: 1;
}
CSS containを活用する
containプロパティは、要素のレイアウト計算のスコープをブラウザに伝えるためのものです。画像コンテナにcontain: layout styleを指定すると、その要素内のレイアウト変更が外部に影響を与えないことをブラウザに保証し、レンダリングのパフォーマンスが向上します。
/* 画像カードのレイアウトシフトを完全に防止 */
.card {
contain: layout style;
content-visibility: auto; /* ビューポート外のレンダリングをスキップ */
contain-intrinsic-size: 0 400px; /* 推定サイズを指定 */
}
.card__image {
aspect-ratio: 4 / 3;
width: 100%;
height: auto;
object-fit: cover;
background: linear-gradient(135deg, #e0e0e0 25%, #f5f5f5 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite; /* ローディングアニメーション */
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.card__image.loaded {
animation: none;
background: none;
}
このパターンを使うと、画像の読み込み中はシマーアニメーション(キラキラ光るプレースホルダー)が表示され、読み込み完了後に画像が表示されます。見た目も良いし、CLSもゼロ。一石二鳥ですね。
第8章:実践パフォーマンス監査チェックリスト
LCP画像の監査ステップ
ここからは実践編です。自分のサイトの画像パフォーマンスを改善するための、ステップバイステップの手順をまとめました。これを順番にやるだけで、かなりの改善が期待できます。
-
LCP要素を特定する
Chrome DevToolsの「Performance」タブでページを録画し、「Timings」セクションでLCPマーカーを確認。関連する要素をクリックすれば、何がLCP要素かわかります。
-
LCP画像のファイルサイズを確認する
DevToolsの「Network」タブで画像を絞り込み、LCP画像のサイズを確認。デスクトップなら200KB以下、モバイルなら100KB以下が理想的です。
-
画像フォーマットを確認する
まだJPEG/PNGだけを配信していないか? AVIFとWebPのフォールバックが設定されているか確認しましょう。
-
fetchpriorityとloading属性を確認する
LCP画像に
fetchpriority="high"とloading="eager"が設定されているか?loading="lazy"が誤って付いていないか? -
レスポンシブ画像の設定を確認する
srcsetとsizesが正しく設定されているか? 不必要に大きい画像がモバイルに送られていないか? -
width/height属性の確認
すべての
<img>タグにwidthとheightが設定されているか? CLSの原因になっていないか? -
プリロードの検討
CSSの背景画像やJSで動的に読み込む画像がLCP要素になっていないか? なっているなら
<link rel="preload">を追加。
Lighthouseの活用テクニック
Lighthouseは最も手軽なパフォーマンス診断ツールですが、効果的に使うにはいくつかコツがあります:
- シークレットモードで実行する:ブラウザ拡張がパフォーマンスに干渉するのを防ぐ
- モバイルモードで確認する:Lighthouseのデフォルトはモバイルシミュレーション。デスクトップでは良好でもモバイルで不良になるケースが多い
- 複数回実行する:ネットワーク状況によって結果が変動するため、最低3回は実行して中央値を見る
- 「画像の最適化」セクションに注目:次世代フォーマットの提案、適切なサイズの提案、遅延読み込みの提案が個別に表示される
Lighthouseの監査結果をプログラムで取得することもできます:
// Lighthouse をプログラムから実行する例
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
async function runAudit(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const result = await lighthouse(url, {
port: chrome.port,
onlyCategories: ['performance'],
formFactor: 'mobile',
screenEmulation: {
mobile: true,
width: 412,
height: 823,
deviceScaleFactor: 2.625
},
throttling: {
rttMs: 150,
throughputKbps: 1638.4,
cpuSlowdownMultiplier: 4
}
});
const { lhr } = result;
// LCP関連の監査結果を抽出
const lcpValue = lhr.audits['largest-contentful-paint'].numericValue;
const lcpDisplay = lhr.audits['largest-contentful-paint'].displayValue;
console.log(`LCP: ${lcpDisplay} (${lcpValue.toFixed(0)}ms)`);
// 画像関連の監査を確認
const imageAudits = [
'modern-image-formats', // 次世代画像フォーマット
'uses-optimized-images', // 画像の効率的なエンコード
'uses-responsive-images', // 適切なサイズの画像
'offscreen-images', // オフスクリーン画像の遅延読み込み
'unsized-images', // 明示的なサイズのない画像
'prioritize-lcp-image' // LCP画像のプリロード
];
imageAudits.forEach((auditId) => {
const audit = lhr.audits[auditId];
if (audit) {
const status = audit.score === 1 ? '✅' : audit.score === null ? '➖' : '❌';
console.log(`${status} ${audit.title}: ${audit.displayValue || ''}`);
}
});
await chrome.kill();
}
runAudit('https://example.com').catch(console.error);
CrUXデータで実ユーザーの体験を把握する
Lighthouseの結果はあくまでラボ環境のシミュレーションです。実際のユーザー体験を知るには、Chrome User Experience Report(CrUX)のデータが不可欠。
CrUXは、Chromeユーザーから匿名で収集された実際のパフォーマンスデータで、28日間のローリング平均として集計されます。つまり、今日の改善が数値に反映されるまで最大28日かかるということ。これは覚えておいてください。改善したのに数字が変わらない!とならないように。
CrUXデータの確認方法:
- PageSpeed Insights:最も手軽。URLを入力するだけでCrUXデータとLighthouseの結果が両方見られる
- Google Search Console:Core Web Vitalsレポートで、サイト全体のURLごとの合否状況を確認
- CrUX API:プログラムからアクセスして、ダッシュボードに組み込める
- BigQuery:CrUXの生データにSQLでアクセス。競合サイトとの比較分析も可能
// CrUX API を使ったLCPデータの取得
async function getCrUXData(url) {
const API_KEY = 'YOUR_API_KEY';
const endpoint = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${API_KEY}`;
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: url,
formFactor: 'PHONE',
metrics: [
'largest_contentful_paint',
'cumulative_layout_shift',
'interaction_to_next_paint'
]
})
});
const data = await response.json();
if (data.record) {
const lcp = data.record.metrics.largest_contentful_paint;
console.log('LCP分布:');
console.log(` Good (≤2500ms): ${(lcp.histogram[0].density * 100).toFixed(1)}%`);
console.log(` Needs Improvement: ${(lcp.histogram[1].density * 100).toFixed(1)}%`);
console.log(` Poor (>4000ms): ${(lcp.histogram[2].density * 100).toFixed(1)}%`);
console.log(` 75パーセンタイル: ${lcp.percentiles.p75}ms`);
}
return data;
}
getCrUXData('https://example.com').catch(console.error);
CrUXのp75(75パーセンタイル)が2500ms以下であれば、Core Web VitalsのLCPは「合格」です。p75を使う理由は、「ほとんどのユーザー(75%以上)が良好な体験を得ている」ことを保証するため。
画像パフォーマンス監査チェックリスト(まとめ)
最後に、すべてをまとめたクイックチェックリストを載せておきます。プロジェクトの画像パフォーマンスを評価する際にぜひ活用してください。
- フォーマット:AVIF/WebPを提供している?
<picture>でフォールバックチェーンを組んでいる? - サイズ:
srcsetとsizesでレスポンシブ画像を提供している? 不要に大きい画像を送っていない? - LCP最適化:LCP画像に
fetchpriority="high"を付けている?loading="lazy"を付けていない? - プリロード:LCP画像を
<link rel="preload">で先読みしている? - Lazy Loading:ファーストビュー外の画像は
loading="lazy"で遅延読み込みしている? - CLS対策:すべての
<img>にwidth/heightを設定している?aspect-ratioを使っている? - 圧縮品質:品質設定は適切? WebPなら70-80、AVIFなら45-55程度が目安
- CDN:画像CDNを使っている? エッジキャッシュが有効?
- デコード:
decoding="async"を設定している? - 監視:RUMでLCPを継続的に計測している? CrUXデータを定期的に確認している?
まとめと今後の展望
ここまで、2026年における画像最適化とLCP改善のテクニックをかなり幅広く解説してきました。改めてポイントを整理すると:
- LCPの70%は画像が要因。画像を最適化すればLCPは改善する
- AVIFファースト戦略でファイルサイズを最大50%削減。WebPは安定のフォールバック
- srcset/sizesで適切なサイズの画像を配信し、帯域幅の無駄をなくす
- fetchpriority="high"でLCP画像の読み込みを最優先に(Google Flightsで0.7秒改善の実績)
- LCP画像には絶対にlazy-loadしない。ファーストビュー外のみに適用
- 画像CDNを使えば最適化の多くを自動化できる
- width/heightとaspect-ratioでCLSを完全に防止
- RUMとCrUXで実ユーザーのパフォーマンスを継続的に監視
AI駆動の画像最適化:これからの展望
2026年後半以降、注目すべきトレンドとしてAI駆動の画像最適化があります。すでにいくつかのCDNやツールが導入を始めていますが、今後はこんな機能が当たり前になっていくんじゃないかと思います:
- AIによる知覚品質の最適化:画像の内容(顔、テキスト、風景など)を認識し、領域ごとに最適な圧縮率を自動適用。人間の目に重要な部分は高品質を維持しつつ、背景は積極的に圧縮
- LCP要素の自動検出:ページのレイアウトを解析し、LCP画像を自動特定。
fetchpriorityやプリロードの設定を自動的に最適化 - リアルタイムの適応最適化:ネットワーク状況やデバイスの処理能力に応じて、配信する画像の品質やサイズをリアルタイムに調整
- 自動alt属性生成:画像の内容をAIが解析し、適切なalt属性テキストを自動生成。アクセシビリティの向上にも貢献
- プログレッシブ画像生成:AIが低解像度のプレースホルダー(LQIP: Low Quality Image Placeholder)を自動生成し、段階的に高解像度に置き換え
ただし、これらの技術が成熟するまでの間は、この記事で解説した基本的なテクニックが引き続き最も重要です。<picture>要素によるフォーマットフォールバック、srcset/sizesによるレスポンシブ画像、fetchpriorityとpreloadによるLCP最適化。これらは「枯れた技術」ではなく、2026年以降も長く使えるWeb標準の基礎です。
画像の最適化は、Webパフォーマンスにおいて最もコストパフォーマンスの高い投資だと個人的には思っています。一度しっかり設定してしまえば、その後のメンテナンスコストは低い。それでいて効果は大きい。もし今まで手を付けていなかったなら、今日から始めてみてください。きっと数値に目に見える変化が現れるはずです。