Back to posts

Astroの画像最適化を標準の astro:assets に寄せる

このサイトは Gatsby から Astro 5 に移行したのですが、移行で地味に楽になったのが画像まわりでした。Gatsby 時代は gatsby-plugin-image に gatsby-plugin-sharp、gatsby-source-filesystem といったプラグインの組み合わせで画像を扱っていて、表示を変えるたびに GraphQL のクエリまで触る必要がありました。移行全体の判断については、GatsbyからAstroへ移行した理由:判断軸の整理にまとめています。

Astro では画像最適化が astro:assets という標準機能として入っています。プラグインを追加で入れるのではなく、置き場所とコンポーネントの選び方さえ決めておけば、あとはビルド時に変換がかかります。このサイトでも、アイキャッチなどの画像はこの標準機能に寄せる方針にしました。そのときの判断をメモしておきます。

まず「置き場所」で決まる

Astro の画像は、ファイルをどこに置くかで扱いが大きく変わります。最適化されるかどうかは、コンポーネント以前に置き場所で決まると思ってよいです。

置き場所最適化参照のしかた向いているもの
src/ 配下(例:src/assets/)される(変換・リサイズ・バンドル対象)ファイルを import して渡すアイキャッチ、記事内画像、コンポーネントで使う画像
public/ 配下されない(そのままコピーされる)URL パスで参照(/images/xxx.jpg)favicon、OGP 画像など「加工せずそのまま出したい」もの

public/ に置いた画像は、<Image /> で表示しても最適化されません。ビルド成果物に無加工でコピーされるだけです。「コンポーネントを使っているのに軽くならない」という場合は、だいたい置き場所が public/ になっています。

逆に、加工したくないものまで src/ に入れる必要もありません。favicon のようにブラウザから直接参照されるファイルは public/ の方が素直です。

ポイント:記事で使う画像は src/、加工なしで出したいファイルだけ public/。この線引きを先に決めておくと、後の判断がだいぶ楽になります。

<Image /> が基本形

単一フォーマットでよければ <Image /> が基本です。src/ の画像を import して渡すだけで、ビルド時に WebP への変換がかかり、width / height は原画のサイズから自動で推定されます。

---
import { Image } from 'astro:assets';
import cover from '../assets/cover.jpg';
---

<Image src={cover} alt="記事のアイキャッチ画像" />

出力される <img> には、loading="lazy" と decoding="async"、そして推定された width / height が付きます。幅と高さが先に分かるので、画像の読み込み後にレイアウトがずれる CLS(Cumulative Layout Shift)を避けられる、というのが実は一番ありがたいところです。手書きの <img> だと、この属性の付け忘れがそのまま CLS につながります。

なお alt は必須で、付け忘れるとエラーで教えてくれます。装飾だけの画像なら alt="" を明示します。

Markdown の記事ファイルでも、src/ 配下の画像を ![alt](../assets/photo.jpg) と相対パスで書けば最適化の対象になります。ただし <Image /> コンポーネント自体は .md では使えないので、細かい制御が要る場合は MDX が必要です。

複数フォーマットを出したいときは <Picture />

AVIF や WebP など、複数のフォーマットを用意してブラウザに選ばせたいときは <Picture /> を使います。<picture> タグが生成され、指定したフォーマットが <source> として並び、元フォーマットがフォールバックの <img> になります。

---
import { Picture } from 'astro:assets';
import photo from '../assets/photo.png';
---

<Picture src={photo} formats={['avif', 'webp']} alt="写真の説明" />

使い分けを整理するとこうなります。

項目<Image /><Picture />
出力される要素<img><picture>(<source> + <img>)
フォーマット1種類(デフォルトは WebP)複数指定可(AVIF / WebP など)
用途通常の画像全般画質と容量を詰めたい大きな写真など
生成ファイル数少ないフォーマット × サイズの分だけ増える

個人的な線引きは「迷ったら <Image />」です。<Picture /> は生成されるファイルが増えてビルドも重くなるので、トップページの大きなビジュアルや写真が主役の記事など、効果が見込める場所に絞っています。ビルドの重さの見直し全般は、Astroのビルドを速く保つための見直しポイントで扱っています。ブログの記事内画像は、だいたい <Image /> で足ります。

サイズ違いを配るときは widths

表示幅に応じた複数サイズを配りたい場合は、widths と sizes をセットで指定します。

<Image
  src={cover}
  widths={[400, 800, 1200]}
  sizes="(max-width: 800px) 100vw, 800px"
  alt="記事のアイキャッチ画像"
/>

知っておくとよい挙動が2つあります。

  • widths に原画より大きい値を書いても、その分は無視されます(拡大生成はされません)。つまり「あとから大きくもできるように」と原画を巨大にしておく意味は薄いです
  • 固定レイアウトの画像(アイコンなど、幅が変わらないもの)なら、widths ではなく densities={[1.5, 2]} で解像度違いを出す書き方もあります。両方の同時指定はできません

Astro 5.10 以降は layout プロパティ(constrained / full-width / fixed)もあり、指定すると srcset と sizes を自動生成してくれます。手書きの widths は「自動生成の結果を上書きしたいとき」の手段、と覚えておくと混乱しません。

リモート画像は「許可したドメインだけ」が最適化される

外部 URL の画像も <Image /> に渡せますが、最適化されるのは設定で許可したドメインだけです。許可していないドメインの画像は無加工で表示されます(CLS 対策の属性付与自体は効きます)。

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    domains: ['images.example.com'],
  },
});

パス単位で絞りたい場合は image.remotePatterns もあります。また、リモート画像はローカルのように寸法を自動で読めないので、width と height の指定が必要です。寸法が分からない・変わる可能性がある場合は inferSize を付けると取得時に推定してくれます。

注意:許可ドメインは「誰の画像でも最適化してあげる」窓口にもなります。自分が管理している配信元だけを許可して、広げすぎないのが無難です。

やっておかない方がよいこと

最後に、運用していて「これは逆効果だな」と思うパターンを挙げておきます。

  • 巨大な原画をそのままリポジトリに置く:前述のとおり原画を超えるサイズは生成されませんし、変換対象が大きいほどビルド時間と成果物のファイル数が膨らみます。表示上限の少し上くらいの解像度に抑えて取り込む方が、結局は速いです
  • 最適化前提で public/ に置く:何度も書きますが、public/ は無加工です。「Astro がよしなにしてくれるはず」という置き方をすると、原画サイズのまま配信されます
  • 全部の画像を <Picture /> にする:フォーマット × サイズの組み合わせ分だけファイルが生成されます。全画像にやるとビルドが確実に重くなります
  • width / height を手で書いて原画と食い違わせる:src/ の画像なら自動推定に任せる方が安全です。手書きが必要なのは public/ とリモート画像のときだけ、と覚えています

このサイトは全ページ静的生成なので、画像変換はビルド時にまとめて行われます。実行時に変換する仕組みを別途用意しなくてよいのも、静的ブログと astro:assets の相性のよさだと思います。

置き場所を src/ に寄せて、基本は <Image />、必要な場所だけ <Picture /> と widths。このくらいの運用ルールで、個人ブログの画像はほぼ回ります。

参考