blog.s2n.tech

Astroで記事内の画像を最適化する

公開

はじめに

AstroにはContents CollectionというMarkdownやMDXファイルをコレクションとして管理し、CMSのように扱うことができる非常に便利な機能があります。しかし、デフォルトでは記事中の画像の最適化があまり積極的に行われないため、画像の読み込みに時間がかかってしまいます。

そこで、今回は記事内の画像を最適化する方法をいくつか紹介します。

デフォルトの挙動

まずはデフォルトではどのように画像が表示されるのかを見てみましょう。記事中に![alt](./image.png)のように画像を配置した場合、デフォルトではこのようなHTMLが生成されます。

<img
src="/_astro/image.xxx.webp"
alt="alt"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="1920"
height="1080"
/>

<img>タグを見てみると、画像がWebPに変換され、画像サイズや遅延読み込みなどの属性が設定されていることがわかります。

このままでもWebP変換といった最低限の最適化が行われますが、画像サイズは元画像のままなので、元画像が非常に大きなサイズの場合は、結局画像の読み込みに時間がかかってしまいます。また、Markdownの記法では画像のサイズを指定することができず、通常のHTML記法ではローカル画像の参照ができません。(/publicフォルダは使用できますが、今度は最適化が行われなくなります。)

astro.config.jsでレスポンシブ画像の設定を行う

astro.config.jsにはimageというオプションがあり、画像最適化の細かい設定やデフォルトの挙動を設定することができます。

そしてその中にあるlayoutオプションを設定することで、画像のレスポンシブ対応を行うことができます。デフォルトではundefinedとなっているため、レスポンシブ対応が行われません。とりあえずレスポンシブにしたい場合は"constrained"に設定しておくとよいです。その他の設定はドキュメントを参照してください。

astro.config.js
export default defineConfig({
// ...
image: {
layout: "constrained",
},
// ...
});

このオプションを設定した場合、生成されるHTMLはこのようになります。

<img
src="/_astro/image.xxx.webp"
srcset="
/_astro/image.xxx.webp 640w,
/_astro/image.xxx.webp 750w,
/_astro/image.xxx.webp 828w,
/_astro/image.xxx.webp 1080w,
/_astro/image.xxx.webp 1280w,
/_astro/image.xxx.webp 1668w,
/_astro/image.xxx.webp 1920w
"
alt="alt"
loading="lazy"
decoding="async"
fetchpriority="auto"
sizes="(min-width: 1920px) 1920px, 100vw"
style="--fit: cover; --pos: center;"
data-astro-image="constrained"
width="1920"
height="1080"
/>

先程の<img>タグにあった属性に加えて、srcsetsizes属性などが設定されていることが分かります。これらの属性によって、デバイスのビューポートに応じて最適なサイズの画像が読み込まれるようになります。これらの属性の詳細に関してはICS MEDIAの記事が参考になるので、そちらをご覧ください。

imgタグのsrcset・sizes属性とpictureタグの使い方 - レスポンシブイメージで画像表示を最適化 - ICS MEDIA 日本のウェブサイトにおけるスマートフォン・タブレットユーザーのシェアは約47%であり、ウェブデザインはレスポンシブ対応しモバイルを意識した設計が必須です(参照「StatCounter」)。HTMLの「レスポンシブイメージ」を使えば、HTMLのタグだけで表示端末にあわせた画像を配信できます。 ics.media

MDXのカスタムコンポーネントを使用する

astro.config.jsを使用した画像のレスポンシブ化はお手軽で便利ですが、一括設定のみで記事以外の画像もレスポンシブ対応になってしまいますし、srcsetsizes属性の調整が難しいです。

記事内の画像のみをレスポンシブ対応にしたい場合はMDXのカスタムコンポーネント機能を利用するのが良いでしょう。 MDXの場合、<Content />コンポーネントのcomponentsに上書きしたいコンポーネントを設定することができます。残念ながらMarkdownの場合はコンポーネントの上書きができないため、astro.config.jsの設定か、.md.mdxに書き換える必要があります。

まずは、上書きするためのImg.astroを作成します。これは実際にこのブログのソースコードから抜粋したものです。

Img.astro
---
import type { ImageMetadata } from "astro";
import { Image } from "astro:assets";
interface Props {
src: ImageMetadata | string;
alt: string;
title?: string;
}
const { src, alt, title } = Astro.props;
---
{
typeof src !== "string" ? (
<Image
src={src}
widths={[324, 690, 830, 830 * 2]}
sizes="(max-width: 928px) 100vw, 830px"
alt={alt}
title={title}
/>
) : (
<Image inferSize src={src} alt={alt} title={title} />
)
}

このブログの場合、記事全体の幅は830px以上になることはないため、sizesで830pxを最大幅としています。画面幅が928px以下の場合は記事の幅が画面幅に合わせて縮むため、100vwとしています。

widthsは高dpiデバイスでの表示も考え、830pxの2倍までのサイズを指定しています。

なお、リモート画像の場合は<Image />inferSizeを指定する必要があるため、typeof src !== "string"で条件分岐を行っています。リモート画像の場合は画像の最適化は行われませんが、生成される<img>タグにwidthheight属性が設定されるため、loading="lazy"が効果的に機能するようになり、CLSを抑えることもできます。

あとは<Content />コンポーネントのcomponentsに上書きしたいコンポーネントを設定するだけです。

---
import { render, getEntry } from "astro:content";
import Img from "./Img.astro";
const post = await getEntry("posts", "id");
const { Content } = await render(post);
---
<Content components={{ img: Img }} />

これによって、記事内の画像表示にImg.astroが使用されるようになります。

まとめ

Astroのコンテンツコレクションは便利ですが、デフォルトでは画像の最適化があまり行われないため、パフォーマンスが悪くなる可能性があります。今回紹介したような方法で記事内の画像を最適化することで、パフォーマンスを向上させることができます。