Pagespeed Insightsを100点に!Astroの画像最適化

はじめに
Astro でブログ構築をしていく中でLCPの数値に影響していそうな下記2つの警告が出て PagespeedInsights のパフォーマンスの点数が92点まで下がってしまいました
- Properly size images
Serve images that are appropriately-sized to save cellular data and improve load time.
- Largest Contentful Paint element
This is the largest contentful element painted within the viewport
上記の警告を総合すると、LCP に相当するアイキャッチを含めて画像サイズの最適化ができていなそうなことがわかりました
そこで、現状を整理した上でパフォーマンスを100点に戻すべく改善を行いました
現状
現在運用している Astro ブログは、記事を書く際にフロントマターでアイキャッチを設定して、本文中の画像は Markdown で設定しています。ブログで使う画像は全て public/images/blog 配下に置いています
---
title: "[Astro] ブログにQA機能を追加したいからプラグインを作った"
image: "/images/blog/eyecatch.webp"
date: 2025-01-18
description: "AstroでQAコンテンツを表示するためのRemarkプラグインを実装したので解説しました!"
categories: ["Astro"]
tags: ["Astro", "Markdown"]
type: "blog"
pickup: true
---
// 文中の画像挿入は以下の形式

アイキャッチについてはブログごとにフロントマターで設定した image を .astro ファイルで Astro 組み込みの <Image />
コンポーネントに渡して表示しています
---
import { Image } from "astro:assets";
const { blog } = Astro.props;
const { Content } = await blog.render();
const { title, categories, image, date, tags }: BlogData = blog.data;
---
<section class="section">
...
<article>
// アイキャッチはここで設定
{image && (
<Image
src={image}
height={500}
width={1000}
alt={title}
class="mb-4"
loading={"eager"}
/>
)}
// ここで本文を表示
<Content />
</article>
</section>
画像サイズの最適化対応
調査
まず、画像サイズの課題を解消するために画像の最適化について調査しました
基本的に画像を配信を行う際はユーザーのスクリーンサイズに合わせたサイズの画像を配信すれば、データ転送量を削減することができ Pagespeed Insights のパフォーマンス改善を行うことができます。しかし、一般的にユーザーのスクリーンサイズに合わせたサイズの画像の配信を実現するためには、事前にサイズの異なる画像を複数用意しておく必要があります
一方、Astro では Astro組み込みの <Image />
コンポーネントを使用を使えば、画像幅を複数指定するだけでフレームワークが自動的にソース画像から指定した幅の画像を生成し、スクリーンサイズに応じて配信画像を切り替えてくれる仕様になっています
公式Doc では以下のように widths プロパティに生成する画像の幅の配列を渡し、sizes プロパティで画面サイズの指定を行っています
---
import { Image } from 'astro:assets';
import myImage from "../assets/my_image.png"; // 画像は1600x900
---
<Image
src={myImage}
widths={[240, 540, 720, myImage.width]}
sizes={`(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, ${myImage.width}px`}
alt="説明文"
/>
上記のような設定を行うと、img タグに変換された時に以下のように画面幅に合わせた画像が生成されます
<img
src="/_astro/my_image.hash.webp"
srcset="
/_astro/my_image.hash.webp 240w,
/_astro/my_image.hash.webp 540w,
/_astro/my_image.hash.webp 720w,
/_astro/my_image.hash.webp 1600w
"
sizes="
(max-width: 360px) 240px,
(max-width: 720px) 540px,
(max-width: 1600px) 720px,
1600px
"
alt="説明文"
width="1600"
height="900"
loading="lazy"
decoding="async"
/>
画像を保存する場所
自分の Astro ブログで上記の対応を行いましたが「Properly size images」や「Largest Contentful Paint element」の警告は消えませんでした
原因は簡単で、画像を /public 配下に置いており画像処理がされていなかったからです。公式Doc でも説明されている通り、/public ディレクトリのファイルは常にそのままビルドフォルダにコピーされ処理されることはないようです
Astro では、画像に対する処理を避けたい場合や画像へのリンクを直接公開したい場合は画像を public/ フォルダに保存し、それ以外の場合ローカル画像は /src 配下に置いた方が良さそうです
そのため、ブログで使用するローカル画像については public/images/blog から /src/images/blog 配下に移動しました
それに伴い、mdx ファイルのフロントマターで設定していたアイキャッチ画像や文中の画像については /src 配下の画像を利用するように修正しています
---
title: "[Astro] ブログにQA機能を追加したいからプラグインを作った"
image: "../../images/blog/eyecatch.webp"
date: 2025-01-18
description: "AstroでQAコンテンツを表示するためのRemarkプラグインを実装したので解説しました!"
categories: ["Astro"]
tags: ["Astro", "Markdown"]
type: "blog"
pickup: true
---
// 文中の画像挿入は以下の形式

アイキャッチ画像が表示できない
上記の対応でブログで利用するアイキャッチ画像と文中の画像は /src 配下の画像を参照するようにしました。しかし、こちらの対応を行ったことで下記のエラーが出るようになってしまいました
Image
’s andgetImage
’ssrc
parameter must be an imported image or an URL, it cannot be a string filepath
こちらについては 公式Doc でも説明されており、<Image />
コンポーネントの src プロパティは、インポートされた画像かURLでなければいけないそうです
---
import { Image } from "astro:assets";
import myImage from "../my_image.png";
---
<!-- GOOD: `src` is the full imported image. -->
<Image src={myImage} alt="Cool image" />
<!-- GOOD: `src` is a URL. -->
<Image src="https://example.com/my_image.png" alt="Cool image" />
<!-- BAD: `src` is an image's `src` path instead of the full image object. -->
<Image src={myImage.src} alt="Cool image" />
<!-- BAD: `src` is a string filepath. -->
<Image src="../my_image.png" alt="Cool image" />
こちらについてはドキュメントで解決策も提示されており、コンテンツコレクションのローカル画像の場合は、image() スキーマヘルパーを使用して画像をインポートできます
公式Doc でいうとこちらの項目に相当しており、コンテンツコレクションのスキーマで対象のプロパティに image() ヘルパーを適用することで画像をインポートできるようです
import { defineCollection, z } from "astro:content";
const blogCollection = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
cover: image().refine((img) => img.width >= 1080, {
message: "カバー画像は幅1080ピクセル以上でなければなりません!",
}),
coverAlt: z.string(),
}),
});
export const collections = {
blog: blogCollection,
};
自分のブログについてもアイキャッチ画像のプロパティに上記の対応を行いましたが、インポートできておらずエラーを解決することができませんでした。こちらについては別途深掘り調査が必要そうです…
アイキャッチ画像を動的にインポートする
上記の対応でアイキャッチ画像を表示することができませんでした。ゴールは mdx ファイルのフロントマターで設定したアイキャッチ画像を .astro でインポートして <Image />
コンポーネントの src プロパティに渡してあげることで、課題は .astro で画像を動的にインポートできないことです
Astro にこの課題を解決できる機能はないか探したところ、公式Doc に画像の動的インポートに関する機能を発見しました
Astro においては、Vite の import.meta.glob 関数を使うことで画像の動的インポートを実現できるそうです
まず、アイキャッチ画像をインポートしたいファイルで import.meta.glob 関数を使って src/images/blog 配下の全ての画像パスのオブジェクトを取得します。また、以下の例では型定義のために ImageMetadata 型もインポートしています
---
import { Image } from "astro:assets";
const { blog } = Astro.props;
const { Content } = await blog.render();
const { title, categories, image, date, tags }: BlogData = blog.data;
+ import type { ImageMetadata } from 'astro';
+ const images = import.meta.glob<{ default: ImageMetadata }>('/src/images/blog/*.{jpeg,jpg,png,gif.webp}')
---
<section class="section">
...
<article>
// アイキャッチはここで設定
{image && (
<Image
src={image}
alt={title}
class="mb-4"
loading={"eager"}
/>
)}
// ここで本文を表示
<Content />
</article>
</section>
images 変数には以下のように指定ディレクトリに保存されている画像パスのオブジェクト一覧が格納されます
{
'/src/images/blog/blog-1.webp': [Function: /src/images/blog/blog-1.webp],
'/src/images/blog/blog-2.webp': [Function: /src/images/blog/blog-2.webp],
...
}
特定の画像を取得する際は、<Image />
コンポーネントの src 属性の中に images オブジェクトを渡し、画像パスをブラケット記法で指定します
---
import { Image } from "astro:assets";
const { blog } = Astro.props;
const { Content } = await blog.render();
const { title, categories, image, date, tags }: BlogData = blog.data;
import type { ImageMetadata } from 'astro';
const images = import.meta.glob<{ default: ImageMetadata }>('/src/images/blog/*.{jpeg,jpg,png,gif.webp}')
---
<section class="section">
...
<article>
// アイキャッチはここで設定
{image && (
<Image
src={images[image]()}
alt={title}
class="mb-4"
loading={"eager"}
/>
)}
// ここで本文を表示
<Content />
</article>
</section>
あとは、アイキャッチとして無効なファイルパスが設定された場合のエラー処理を追加してあげれば設定は完了です
---
import { Image } from "astro:assets";
const { blog } = Astro.props;
const { Content } = await blog.render();
const { title, categories, image, date, tags }: BlogData = blog.data;
import type { ImageMetadata } from 'astro';
const images = import.meta.glob<{ default: ImageMetadata }>('/src/images/blog/*.{jpeg,jpg,png,gif.webp}')
+ if (!images[image]) throw new Error(`"${image}" does not exist in glob: "src/assets/*.{jpeg,jpg,png,gif,webp}"`);
---
<section class="section">
...
<article>
// アイキャッチはここで設定
{image && (
<Image
src={images[image]()}
alt={title}
class="mb-4"
loading={"eager"}
/>
)}
// ここで本文を表示
<Content />
</article>
</section>
最終的に…
上記の動的インポートの対応を行ったことによって、Pagespeed Insights で指摘されていた2つの警告は解決できました。
結果的に LCP の数値も改善され、パフォーマンスの数値も100点に返り咲きました!