Hugoブログの画像を軽量化した記録|サムネイル分離とPageSpeed改善

Hugoブログの画像を軽量化した記録|サムネイル分離とPageSpeed改善
この記事の概要

この記事は、2026年5月7日にこのブログのトップページで行った改善の記録です。2026年9月にデザインを変更したため、ここに出てくるヘッダーの背景画像や、画像付きの大きな記事カードは今のトップページにはありません(デザイン変更の記事)。記事中の数値は当時の計測結果で、現在のページの性能値ではありません。

点数は出たけど、何が起きたのかわからなかった

PageSpeed Insights は、GoogleがWebページの読み込み速度を100点満点で評価してくれる無料ツールです。スマホでの表示速度(モバイル)とパソコンでの表示速度(デスクトップ)を別々に測定してくれます。

昔々、ブックマークに入れていたものを掘り起こしたわけですね。

このブログのトップページ(https://blog.keyuki.net/)をモバイルで測定したら、こんな数字が出ました(2026年5月7日 21:32)。

  • Performance: 67(100点満点。低いほど遅い)
  • LCP: 175.2秒
  • Speed Index: 5.5秒
  • Total network payload: 39,910 KiB(約39MB)

LCP(Largest Contentful Paint)は、ページを開いてから、画面内で一番大きなコンテンツが表示されるまでの時間です。175秒は、ほぼ3分です。

ただ、これは誰かが実際に3分待ったという意味ではありません。PageSpeed Insightsのモバイル計測は、中程度の性能のスマホと低速なモバイル回線(下り1.6Mbps)を想定したシミュレーションで数値を出しています(PageSpeed Insightsの説明、Lighthouseの通信制限の説明)。

この記事の数値は、各段階で1回ずつ計測し、当時の作業メモに転記した値です。今回の改稿では元のPageSpeedレポートを再確認できていないため、LCPが175.2秒になった原因は特定できていません。

パフォーマンス67という点数は見れば悪いとわかりますが、最初は何を直せばいいのかよくわかりませんでした。

当時の結果で目についたのは、トップページが大量の画像を読み込んでいたことでした。まずは、この転送量を減らすことにしました。


トップページが約39MBあった

サーバー応答は速かったです。JavaScriptのブロックも致命的ではありませんでした。大きかったのは、ひたすら画像でした。

当時のトップページには最新記事のカードが並び、各カードに記事のアイキャッチ画像を表示していました。見た目は小さなサムネイルですが、裏では記事ページ用のPNGをそのまま読み込んでいました。PageSpeedの結果では、1枚あたり約4.9〜6.0MBのPNGが6枚読み込まれていました。

小さく表示しているから軽い、とは限らない。

ブラウザは width を小さく指定されても、画像ファイルのサイズは変わりません。表示を縮小しているだけで、5MBのPNGは5MBのまま読み込まれます。


まず背景画像を軽くした

最初に手をつけたのは、ヘッダーの背景画像でした。

当時ヘッダーに使っていた hero-background.jpg は、7200×4800ピクセルで約7.6MBありました。見た目はきれいですが、7MBは明らかに過剰です。

これを1600×1067ピクセルに縮小したうえで、WebP(ウェブピー)形式に変換しました。WebPはGoogleが開発した画像フォーマットで、Googleの説明では、同じ画質ならJPEGより25〜34%小さくできるとされています。変換後は約283KB、元の約1/27になりました。形式の変更だけでここまでは小さくならないので、画像の縦横を約4.5分の1に縮めた効果が大きかったはずです。

テンプレートでは、背景画像のパスを差し替えました(当時のヘッダーのテンプレート)。

-{{ $hero_image := "/images/hero-background.jpg" }}
+{{ $hero_image := "/images/hero-background.webp" }}

このとき一緒にデプロイしたのは、背景画像の変更のほか、記事カードの画像への loading="lazy" などの追加、Microsoft Clarityの遅延読み込み、文字色のコントラスト調整です。デプロイ後に計測し直した結果(22:18)はこうでした。

  • Performance: 67 → 75
  • LCP: 175.2秒 → 5.0秒
  • Total network payload: 39,910 KiB → 32,419 KiB

LCPは大きく改善しました。転送量が減った分(7,491 KiB)は、ほぼ背景画像が小さくなった分に相当します。LCPの改善も背景画像の効果が大きいと考えていますが、ほかの修正も同時に入れていたので、どの修正がどれだけ効いたかまでは切り分けていません。

ただ、ペイロード(計測中に読み込んだデータの総量)はまだ32MBあります。トップページの記事カード画像がほぼそのまま残っていたからです。


lazy loadingだけでは足りなかった

背景画像の変換と同じデプロイで、記事カードの画像に loading="lazy" と decoding="async" も加えていました。当時のテンプレートは次のような形です(一部の属性は省略しています)。

<img src="{{ $featured_image }}" class="img br2 summary-img" alt="{{ .Title }}"
     decoding="async" loading="lazy">

$featured_image には、テンプレートの中で取り出した記事の featured_image、つまり記事ページ用の大きなアイキャッチ画像のURLが入っています。

それぞれの属性の意味はこうです。

  • loading="lazy" — 画面の外にある画像は、スクロールして近づくまで読み込まない
  • decoding="async" — 画像のデコード(展開処理)がページ描画をブロックしにくくなる

ただ、ここで気づいたことがあります。

読み込み方の改善と、画像ファイルそのものを軽くすることは別の話だった。

loading="lazy" は読み込みのタイミングを遅らせるだけで、ファイルサイズは変わりません。PageSpeedの「Total network payload」は、計測中にページが読み込んだデータの合計です。lazyを付けても、読み込まれる画像は5MBのPNGのままです。実際、この段階でも転送量は32,419 KiBあり、カード画像の多くはそのまま読み込まれていました。


画像の役割を3つに分けた

ここまでやって、問題の本質が見えてきました。featured_image(記事のメイン画像を指定するフィールド)という1つのフィールドが、複数の場所で使われていました。

  • 記事ページで大きく表示される
  • トップページの一覧カードでも同じ画像が表示される

SNSカード用の画像は、前日の記事で images として分けたばかりでした。残っていたのは、記事ページで「大きく見せるための画像」と、一覧で「小さく見せるための画像」を同じファイルにしていたことです。5MBのPNGをどちらにも使っていたのが、ペイロードの重さの原因でした。

そこで、フロントマター(記事ファイルの先頭にある設定ブロック)に役割ごとのフィールドを分けることにしました。

featured_image: https://images.keyuki.net/uploads/2026/05/slug-eyecatch.png
thumbnail_image: https://images.keyuki.net/uploads/2026/05/slug-thumb.webp
images:
  - https://images.keyuki.net/uploads/2026/05/slug-og.jpg
フィールド 役割
featured_image 記事ページで大きく見せる画像
thumbnail_image トップページや一覧で小さく見せる画像
images SNSカード用の画像

画像ファイルはどれもCloudflare R2に置いて、URLで参照しています。置き方や命名ルールは、R2で画像を管理する記事にまとめました。

PageSpeedに直接効くのは thumbnail_image です。幅760px・品質78のWebPで、ファイルサイズを小さく抑えた画像を用意しました。featured_image に5MBの高解像度画像を置いたままでも、一覧カードではその画像を読まないようにします。

同じデプロイで、カードの画像に width="760" と height="424" も付けました。ブラウザが先に画像の枠サイズを確保できるので、レイアウトのズレが減ります。また、PageSpeedがヘッダーの背景画像をLCPの対象として検出し、優先して読み込むよう提案していたので、<link rel="preload"> に fetchpriority="high" を付けて背景画像を先に読み込む指定も加えました。

見た目は同じ「記事の画像」でも、ページ内での役割は違う。大きく見せる画像と、一覧で小さく見せる画像を同じファイルにしていたのが重さの原因だった。

古い記事はどうなる?

全記事を一度に直せるわけではありません。そこで、テンプレートは thumbnail_image を優先しつつ、なければ featured_image を使うように組みました。thumbnail_image がある記事から順に軽くなり、ない記事は従来通り表示されます。このときサムネイルを用意したのは、トップページに並んでいた6記事です。

サムネイル導入後の計測結果

サムネイルを設定してデプロイしたあと、PageSpeedで再測定しました(23:39)。

  • Total network payload: 32,419 KiB → 636 KiB
  • FCP(First Contentful Paint、最初のコンテンツが表示されるまでの時間): 2.4秒 → 1.0秒
  • Speed Index: 5.1秒 → 2.3秒

PageSpeedのリソース一覧に表示されたサムネイルのサイズはこんな感じでした。

ファイル サイズ
m4-mac-mini-workstation-thumb.webp 約36 KiB
nasa-clean-air-plants-thumb.webp 約30 KiB
desktop-title-size-research-thumb.webp 約25 KiB
analytics-clarity-cloudflare-thumb.webp 約17 KiB
mobile-heading-size-research-thumb.webp 約15 KiB
x-card-og-image-hugo-ananke-thumb.webp 約14 KiB

もともと5MB前後あったPNG画像が、一覧カードでは14〜36 KiBのWebPに置き換わりました。


サムネイルを入れたら、今度は縦長に見えた

thumbnail_image を追加したあと、トップページのカード画像が縦長に見える問題が起きました。

もともと object-fit: cover(画像をトリミングして枠に合わせるCSS指定)はしていましたが、カード画像の枠(コンテナ)の高さが画像のアスペクト比に引きずられていました。縦長の画像が入ると、カードの縦幅がそれに合わせて伸びてしまいます。

修正は、カードの枠側に aspect-ratio: 16 / 9 を固定することでした(当時のCSSです)。

.summary-image-link {
  aspect-ratio: 16 / 9;  /* 枠を16:9に固定 */
  overflow: hidden;       /* はみ出た部分を隠す */
}

.summary-img {
  display: block;
  object-fit: cover;  /* 画像をトリミングして枠に収める */
  width: 100%;
  height: 100%;
}

速度改善の副作用で見た目のバグが出た、という話です。性能改善と見た目の確認はセットでやる必要があります。


Clarityとコントラストも整えた

ついでに2つ対応しました。どちらも、背景画像と同じ1回目のデプロイに含めていたものです。

Microsoft Clarity の遅延読み込み

アクセス解析に使っている Clarity は、最初のページ描画には不要なスクリプトです。requestIdleCallback(ブラウザが手の空いたタイミングを通知する仕組み)を使って、ブラウザの手が空いてから読み込むようにしました。対応していないブラウザは2秒後に読み込みます。

テキストのコントラスト改善

PageSpeedのAccessibilityスコア(アクセシビリティ:どれだけ読みやすいか)が91から96に上がりました。

  • メタ情報(日付・著者など)の文字色: #777 → #666
  • リンクの色: #357edd → #1f5fbf
  • ホバー時: #1753a8

Lighthouseのためだけでなく、モバイルでの読みやすさも上がりました。薄い背景に薄いグレーの文字は、屋外では特に見づらいです。


点数は少し、でも中身は大きく改善した

当時の作業メモに残した3回の計測値を並べると、こうなります。対象はどれも当時のトップページ(https://blog.keyuki.net/)、PageSpeed Insightsのモバイル計測で、各1回です。

  • 変更前(5月7日 21:32)
  • 1回目の修正後(22:18):背景画像の縮小・WebP化、カード画像へのlazyなどの追加、Clarityの遅延読み込み、コントラスト調整
  • サムネイル導入後(23:39):一覧用サムネイル、カード画像のwidth/height、枠の縦横比の修正、背景画像のpreload
指標 変更前 1回目の修正後 サムネイル導入後
Performance 67 75 77
LCP 175.2秒 5.0秒 4.8秒
FCP 2.5秒 2.4秒 1.0秒
Speed Index 5.5秒 5.1秒 2.3秒
TBT 80ms 60ms 280ms
転送量 39,910 KiB 32,419 KiB 636 KiB
Accessibility 91 96 96

サムネイル導入後のPerformanceスコアは75から77に上がりました。

「2点しか上がっていない」と感じるかもしれません。正直、最初はそう思いました。

でも転送量は32MBから636 KiBまで下がっていました。転送量は実際にダウンロードするデータの量なので、スマホで見る人の通信量はかなり減ったはずです。一方で、FCPやSpeed Indexは先ほどのシミュレーションでの値で、実際に見ている人の表示速度を測ったものではありません。

スコアが大きく動かなかった理由は、まだ残っている課題があるからです。

  • LCPがまだ4.8秒 — ヘッダーの背景画像がLCPの対象になっていました
  • TBT(Total Blocking Time)が280msに増加 — JavaScriptが長時間メインスレッドを占有している時間で、次の課題になりそうです(1回の計測なので、ぶれの可能性もあります)
  • gtag/js(Google Analyticsのスクリプト)の未使用コード(約64 KiB)と Ananke テーマの未使用CSS(約12 KiB)が残っています

ただ、これらは当初39MBあった画像ペイロードに比べれば、はるかに小さい問題です。


デスクトップの結果

今回の修正はモバイルのPageSpeedを見て始めたものですが、同じレポートにはデスクトップの結果も出ていました(サムネイル導入後)。

  • Performance: 98
  • LCP: 1.1秒
  • FCP: 0.6秒
  • Speed Index: 0.8秒
  • Total network payload: 636 KiB(モバイルと同じ)

デスクトップも同じトップページ・同じ一覧カードを使っているので、転送量はモバイルと同じでした。一覧の画像を軽くすれば、画面サイズに関係なく転送量は減ります。ただ、デスクトップは修正前の値を記録していなかったので、どれだけ改善したかは比べられません。


今回わかったこと

  • PageSpeedの数字は、分解すると意外と素朴な原因に戻る
  • loading="lazy" はタイミングの最適化であって、ファイルを軽くするわけではない
  • サムネイル用の画像を別に持つのは手間だが、一覧ページには効果が大きい
  • 見た目の修正と速度改善は、片方だけ確認して終わりにしない
  • 点数の伸びだけを見ると75→77で小さく見えるが、転送量は32MB→636 KiBまで下がった
  • PageSpeedスコアと、実際にユーザーがダウンロードするデータ量は分けて見る必要がある

まとめ

今回のPageSpeed改善をまとめると:

  1. 背景画像を縮小してWebP化(7.6MB → 283KB)。同時に入れた修正とあわせて、LCP が 175秒 → 5秒に改善
  2. 一覧カードに lazy loading を追加したが、転送量自体は減らない
  3. thumbnail_image を導入して、一覧ページで5MBのPNGを読まないようにした → 総転送量が32MB → 636 KiBに
  4. カード画像の縦長バグをCSS修正(aspect-ratio: 16/9)
  5. Clarityを遅延読み込みに変更
  6. テキストコントラストを改善してAccessibilityが91→96に
  7. サムネイル導入後は、モバイルでPerformance 77・LCP 4.8秒、デスクトップでPerformance 98・LCP 1.1秒

PageSpeedの改善というと難しそうに見えますが、今回やったことを一言で言えば「大きく見せる画像」と「小さく見せる画像」を同じものにしない、というだけでした。 前回に引き続き、こんなこと考えなくてもいいSaaSのありがたみを再認識しました。 でもまあ、学びもありましたね。