第27回 画像表示を見直す―Astroの画像最適化と遅延読み込みでページを軽くする

記事本文の画像をAstro標準の画像処理へ移し、WebP、srcset、sizes、width/height、loading="lazy"を追加。278個の画像ファイルと281件の参照を対象に、容量削減とPC・スマホ表示を確認した記録です。

執筆:ゆまラボ運営者

公開:更新:
第27回 画像表示を見直す―Astroの画像最適化と遅延読み込みでページを軽くする

第26回 記事内リンク切れをビルド時に検出する―記事が増えても404を残さない仕組みを作るでは、生成後のHTMLを確認し、存在しない内部URLへのリンクがあればnpm run buildを失敗させる処理を追加した。

今回は記事本文の画像を見直す。

ゆまラボでは、設定画面やエラー、ブラウザ表示を残すためにスクリーンショットを多く使っている。記事が増えるにつれてpublic/images/blog/配下のPNGも増えたが、本文画像は公開用ファイルをそのままimgで参照していた。

表示自体に問題はない。ただ、元の大きなPNGをそのまま配信する状態で、Astroの画像最適化は本文画像には使えていなかった。

修正前の本文画像はpublic配下のPNGをそのまま参照していた#

修正前の確認には、第25回の404ページの記事を使った。

本文には、存在しないURLへアクセスしたときのブラウザ画面をスクリーンショットとして掲載している。

画像最適化前の記事本文に表示されている404画面のスクリーンショット

画面上は本文幅に収まり、通常の記事画像として表示されている。

Elementsで対象画像を見ると、生成HTMLは次のような形。

<img
  src="/images/blog/web-development/operation/25-custom-404-page/01-before-default-404.png"
  alt="..."
>

画像最適化前のimg要素をChrome DevToolsで確認した画面

この状態では、確認した画像にsrcsetsizesloadingwidthheightは付いていない。

画像ファイルはpublic配下にあり、ブラウザはそこへ置いたファイルをそのまま取得する。

Networkでも修正前の状態を確認した。

画像最適化前のChrome DevTools Network画面を拡大した画像

今回確認するのは、実際に配信される画像サイズがどこまで減るか、PCとスマホで表示が崩れないか、画面下の画像を初期表示ですぐ読み込まない構成にできるか、という点になる。

buildで最適化されていたのはロゴ画像#

修正前のnpm run buildも保存した。

npm run build

実行するとgenerating optimized imagesは表示される。

ただし、この時点でログに出ていた最適化対象はロゴ画像3件。

画像最適化前のnpm run buildでロゴ3件だけがoptimized imagesとして表示された画面

ビルド自体は正常に完了している。

178 page(s) built
Pagefind: Indexed 57 pages

Astroの画像処理自体を使っていないサイトではない。すでにロゴでは使っているが、記事本文の画像がその経路へ入っていなかった。

本文内は281参照、実ファイルは278件#

実装前に本文画像を数えた。

今回数えたのは、本文内のローカル画像参照281件、対象PNG実ファイル278件、元画像総容量44,399,561 bytes。

Lunaの最終報告には「ローカル画像281件を移動」という表現もあったが、内訳は、

blog      259件
articles   19件

で合計278件になる。

Astra側の事前調査でも「PNG 278ファイル、本文内281参照」と確認されたため、この記事では実ファイル278件、本文からの参照281件として記録する。

profile.pngは今回の本文画像処理から外し、既存のpublic側に残した。

本番へ使うLuna版は本文画像をsrc/assetsへ移した#

今回の実装はCodexのモデル比較にも使った。

本番へ反映する実装として選んだのは、GPT-5.6 Lunaを思考量「極高」で動かした結果。

Luna側では、対象の本文画像をsrc/assetsへ移し、Markdownの共通処理からAstro標準の画像処理へ渡す方法を採用した。

主な変更は次の2系統になる。

astro.config.mjs
src/plugins/rehype-article-images.mjs

それに加え、本文画像の実ファイルをpublicからsrc/assets側へ移している。

Markdown本文を1記事ずつ手作業で書き換える方法にはしていない。共通のrehypeプラグインを使い、既存記事の画像記法を処理する。

記事本文の文章、Frontmatter、記事URL、カテゴリ、連載、タグは変更していない。

WebPと4段階のsrcsetを生成する#

Luna版では、本文画像をAstro標準の画像処理へ渡し、WebPを生成する。

代表ページで確認したsrcsetの候補は、

320w
480w
640w
720w

の4段階。

sizesは次の設定になった。

sizes="(min-width: 720px) 720px, 100vw"

PCでは本文の最大表示幅を基準にし、狭い画面ではviewport幅に合わせて候補を選べる。

元画像が大きくても、常にその原寸ファイルを取得する必要はなくなる。

本文画像には元の縦横比をもとにwidthheightも出力する。表示時は既存CSSのレスポンシブ表示を維持し、画像自体は本文幅に収める。

本文画像へloading=“lazy”を追加した#

本文画像には遅延読み込みも追加した。

loading="lazy"

画面下にある画像は、ページを開いた直後にすべて取得するのではなく、必要になるまで読み込みを待てる。

今回の確認では、下方にある本文画像の読み込みがスクロールまで保留されることも確認している。

altは既存Markdownに書いてある内容を維持した。

新しく追加する記事もsrc/assetsへ画像を置く#

今回の実装後は、今後追加する記事の画像配置も変わる。

本文画像用のrehype-article-images.mjsでは、最初に画像のsrcを確認している。

const src = decodeURI(node.properties.src);
if (!src.startsWith('.')) return;

さらに、画像の実体がsrc/assets配下にあるかも確認している。

const assetsRoot = path.resolve('src/assets');

const imagePath = path.resolve(path.dirname(markdownPath), src);
const relativeAssetPath = path.relative(assetsRoot, imagePath);

if (relativeAssetPath.startsWith('..') || path.isAbsolute(relativeAssetPath)) return;

この条件では、従来のように、

![画像](/images/blog/example/image.png)

と書き、画像を、

public/images/blog/example/image.png

へ置いた場合は処理の対象にならない。

src.から始まらないため、その時点で処理を終了する。

今後の記事本文画像はsrc/assets側へ置き、Markdownから相対パスで参照する。

今回の記事なら配置は次の形にする。

src/
├─ content/
│  └─ blog/
│     └─ web-development/
│        └─ operation/
│           └─ 27-astro-image-optimization-lazy-loading.md
└─ assets/
   └─ images/
      └─ blog/
         └─ web-development/
            └─ operation/
               └─ 27-astro-image-optimization-lazy-loading/
                  └─ 01-article-image-before.png

Markdown側は次の相対パスになる。

![画像](../../../../assets/images/blog/web-development/operation/27-astro-image-optimization-lazy-loading/01-article-image-before.png)

この位置関係なら、Markdownから解決した画像パスがsrc/assetsの中に入り、今回追加した共通処理の対象になる。

このプラグイン自体が直接設定しているのは、

layout
width
height
widths

になる。

loading="lazy"をプラグインへ直接書いているわけではないが、今回のビルド後HTMLでは、Astroの画像処理を通った本文画像にloading="lazy"が付いていることを確認した。

今後の記事追加は、

本文画像

src/assets/images/blog/... へ保存

Markdownから相対パスで参照

npm run build

/_astro/ の最適化画像、srcset、loading等を確認

という流れにする。

今回作成する第27回の6枚も、この新しい配置へ変更した。

CodexのLuna/Astra比較記事へ今回追加する画像も同じくsrc/assetsへ置く。

今後、記事生成時に従来のpublic/images/blog/...へ本文画像を追加すると、その画像だけ今回の最適化処理から外れるため注意が必要になる。

元画像44MBから生成候補は約11MBになった#

Luna側で集計した元画像総容量は44,399,561 bytes。

最適化後に生成された本文画像候補は、

11,315,090 bytes

削減量は、

33,084,471 bytes

削減率は74.52%になった。

また、ビルド出力のWebPは、

1,011ファイル
11,325,368 bytes

と報告されている。

元画像1枚を1枚のWebPへ置き換えるだけではなく、複数幅の候補を生成するため、出力ファイル数は元画像数より多くなる。

第25回の記事では122,901 bytesから26,698 bytesになった#

代表ページは、

/blog/25-custom-404-page/

修正前、このページで対象にした記事画像の合計は122,901 bytes。

Luna実装後、PCで実際に選択された画像の合計は、

26,698 bytes

このページ単体では78.28%減っている。

これはサイト全体の表示速度が78.28%速くなったという意味ではない。

今回測っているのは、この代表ページで取得する対象画像のバイト数。HTML、CSS、JavaScript、フォント、通信待ち時間などは別になる。

PCとスマホで本文幅を確認した#

Luna側ではChromeでPCとスマホ相当の両方を確認した。

PC      1440 × 900
スマホ   390 × 844

スマホ側では本文幅343px、画像幅343px。

横スクロールは発生していない。

確認した内容は、

画像が本文幅に収まる
縦横比が変わらない
文字を含むスクリーンショットが読める
画像前後のレイアウトが崩れない
スマホで横へはみ出さない

になる。

生成された8候補についてもHTTP 200とimage/webpを確認した。

buildは178ページで成功した#

実装後も正式な確認は、

npm run build

で行った。

結果は成功。

178 page(s) built
Pagefind: Indexed 57 pages
内部リンク 6,064件
broken links 0件

前回追加した内部リンクチェックもそのまま通っている。

git diff --checkも成功した。

既存のnode scripts/verify.mjsは今回も失敗した。

Markdown 58ファイル
RSS item 57件

重複slugは、

claude-fable-5-1-engineer-view

と報告されている。

これは今回の画像変更より前からある既存状態で、この作業では修正していない。

Luna極高は27分で完走した#

Luna開始前は5時間枠100%残り、週間枠50%残り。実装、ビルド、容量計測、PC・スマホ確認まで終えると、5時間枠88%残り、週間枠48%残りになった。

画像最適化をLuna極高で実行したときのモデル設定と開始前後の利用枠

作業時間は以下のようになった。

開始      2026-09-12 00:56:16.892 +09:00
最終確認  2026-09-12 01:23:20.556 +09:00
実作業時間 27分03.664秒

今回の作業は278個の画像ファイルを扱い、ビルドで1,000件を超えるWebP候補を生成する。これまで比較に使ったTOPの表示変更や404ページ追加より、変更対象も検証量も増えた。

Astraは思考量「軽」で試したが利用枠を使い切った#

同じ作業をGPT-6 Astraでも試した。

今回は利用枠の消費が変わるかを見るため、Astraの思考量を最も低い側の**「軽」**にした。

Astra開始前はLuna終了直後と同じで、5時間枠88%残り、週間枠48%残り。

Astra側も調査と実装は進んだ。

ここは少し意外だった。思考量を「軽」にしても、最終的には利用制限まで到達した。

画面上では、

PNG 278ファイル
本文内281参照
合計44,399,561 bytes

を確認し、Markdown共通処理から画像をAstroへ渡す方法を実装している。

Astra版はLuna版と方向が違い、Markdownや元画像をそのまま残し、設定ファイルと本文画像用プラグインの2ファイルに変更を収めた。

ビルドにも成功し、代表記事では、

PC       30,446 bytes
スマホ   10,474 bytes

まで実測している。

下方画像がスクロールまで読み込み保留になることも確認した。

ただし、要求したすべての検証結果と最終報告を出し切る前に利用制限へ到達した。

Astraを思考量「軽」で実行し、48分後に利用制限へ到達した画面

停止時には5時間枠0%残り、週間枠35%残りと表示された。

Codex画面には「48分間作業しました」と表示され、その直後に「利用制限に達しました」と出ている。

Astra版は何も作れなかったわけではない。ビルド成功とブラウザ確認までは進んだ。

ただ、今回依頼した一連の比較タスクとしては最後まで完走できなかった。思考量を「軽」まで下げても、この規模では5時間枠の残量が足りなかった。

この結果はCodexでAstro改修をLunaとAstraに任せて比較―小さな改修に高性能モデルは必要なのか?へ追記する。

本番へ反映するのはLuna版にした#

今回はLuna側の実装をマージし、本番へ反映することにした。

Luna側では、要求した容量集計、WebP生成、srcsetsizesloading="lazy"、PC・スマホ、画像HTTP 200、正式buildまで一通り確認できている。

Astra軽も異なる実装方法でビルド成功まで進んでおり、その方法自体を否定する結果ではない。

ただし、最終報告前に利用制限へ到達した状態で、その実装を本番採用する判断はしない。

本文画像最適化の変更点#

修正前、記事本文の画像はpublic/images/配下のPNGを通常のimgで直接参照していた。

確認した画像にはsrcsetsizesloadingwidthheightがなく、buildで最適化されていたのはロゴ画像だけ。

今回、本文画像をAstroの画像処理へ渡し、WebP、レスポンシブ画像、遅延読み込み、固有サイズを追加した。

実ファイル278件、本文参照281件、元画像44,399,561 bytesを対象にし、Luna側の集計では生成候補11,315,090 bytesまで減った。

代表記事では122,901 bytesから26,698 bytesになった。

PCとスマホの表示も確認し、画像の横幅超過や404は発生していない。

本番へはLuna側の実装を採用する。

Astraは思考量「軽」で同じ作業を試したが、ビルドとブラウザ確認まで進んだところで5時間枠を使い切った。

参考#