
Astroで画像を出したいけど、public に置くのと src に置くの、どっちが正解なの?

public に入れた画像だけ、いつまで経っても軽くならないんだよね…
Astroには 画像を自動で最適化する仕組み が最初から入っています。
リサイズもWebPへの変換も、こちら側でツールを用意する必要はありません。
ただし、これが効くのは 置き場所と書き方の条件を満たしたときだけ です。
同じ画像でも src/ に置くか public/ に置くかで、まったく別の扱いになります。
私自身このブログの記事画像を src/ 側に置いて運用しているので、その前提も交えつつ、分かれ道と astro:assets の書き方を頭から順に整理していきます。
src/とpublic/で画像の扱いがどう変わるか<Image />と<Picture />の書き方と、必要なpropsの違い- 記事の画像・フロントマターの画像をどう書くか(
image()ヘルパー) - パスが変数になる画像を
import.meta.globで読む方法
記事データそのものの管理方法は、別記事で整理しています。

- コードは Astro v7系(最新は7.3系) の公式ドキュメントに沿った書き方です
<Image />の基本的な書き方は v5系でも同じです(このブログ自体はv7.3系で動いています)- レスポンシブ画像まわりの設定(
image.layoutなど)は Astro 5.10 で追加された機能です - 公式リファレンスは Images guide にあります
Astroの画像は「どこに置くか」で扱いが変わる
Astroで画像を置ける場所は、大きく3つです。src/ の中・public/ の中・外部のURL。
このうち、ビルド時に加工されるのは src/ の中に置いた画像だけです。
src/ に置いた画像|ビルドのときに加工される
src/ の中の画像は、Astroから見ると「ソースコードの一部」です。
import して使うと、ビルド時に圧縮・変換され、ファイル名にハッシュの付いた別ファイルとして書き出されます。
- 形式の変換 … 元がPNG・JPGでも、WebPやAVIFといった軽い形式で出力できる
- リサイズ … 表示に必要な幅だけを書き出し、巨大な元画像をそのまま配らない
- サイズ属性の付与 …
widthとheightが自動で入り、読み込み中のガタつき(CLS)を防げる
public/ に置いた画像|そのまま配信される
public/ は、加工せずにそのまま配る場所です。ファイルはビルド結果にコピーされるだけで、中身は1バイトも変わりません。
そのかわり、URLが /images/logo.png のように固定されます。
faviconやOGP画像のように、URLが変わると困るファイルはここが正解です。
| 観点 | src/ に置く | public/ に置く |
|---|---|---|
| 最適化・リサイズ | ◎ される | × されない |
| ファイル名 | ハッシュ付きに変わる | 置いたまま |
| 参照のしかた | import する | / からのパス |
| 綴りミスの検知 | ◎ ビルドが落ちる | × 落ちない(404になる) |
| 向いているもの | 記事の画像・写真 | favicon・OGP・robots.txt |
らるじゅ迷ったら src/ でいいと思います。
URLを固定したい理由があるものだけ public/ 、という切り分けがいちばん事故が少ないです。
Imageコンポーネントの基本|astro:assets の使い方
最適化を担当するのが、Astro組み込みの astro:assets です。.astro ファイルで import して使います。
1. src/ の画像は import して渡す
---
import { Image } from "astro:assets";
import myImage from "../assets/desk.png";
---
<Image src={myImage} alt="デスクの写真" />
ポイントは2つあります。
1つめは、src に文字列のパスではなく、importした変数を渡すこと。
importされた画像は幅・高さ・形式を持つオブジェクト(ImageMetadata)になっていて、Astroはそこからサイズを読み取ります。だから width と height を書かなくても大丈夫です。
2つめは、alt が必須なこと。読み上げに不要な装飾画像なら alt="" と空文字を明示します。省略するとビルドエラーになります。
2. public/ とリモートの画像はサイズ指定が要る
public/ の画像や外部URLの画像も <Image /> に渡せます。ただし書き方が変わります。
---
import { Image } from "astro:assets";
---
<Image src="/images/stars.png" alt="星空" width="800" height="450" />
こちらは文字列のパスをそのまま渡します。Astroはビルド時にそのファイルの中身を読まないので、サイズを推測できません。
そのため width と height を自分で書く必要があります。
ただし リモート画像に限っては inferSize という逃げ道 があります。
<Image src="https://example.com/photo.jpg" alt="写真" inferSize />
こう書くと、Astroが画像を取りに行ってサイズを読み取ってくれます。
ただし取得できるのは 後述の設定で許可したドメインの画像だけ です。public/ の画像には使えないので、こちらは素直に width と height を書きましょう。
なお、いずれの書き方でも public/ の画像そのものは最適化されません。得られるのは、サイズ属性が入ることによるレイアウト崩れの防止だけです。
3. Picture で AVIF・WebP を出し分ける
複数の形式を出し分けたいときは <Picture /> を使います。<picture> と <source> を組み立ててくれるコンポーネントです。
---
import { Picture } from "astro:assets";
import myImage from "../assets/desk.png";
---
<Picture src={myImage} formats={["avif", "webp"]} alt="デスクの写真" />
formats に並べた順が、そのまま <source> の並び順になります。新しい形式を先に書くのが基本で、対応していないブラウザは後ろの候補に落ちます。
formats を省略した場合は ["webp"] が使われます。1形式で足りるなら、素直に <Image /> を使えば済む話ですね。
記事の中の画像とフロントマターの画像
ブログを作る場合、画像の大半は記事側に出てきます。ここも押さえておきたいところです。
Markdown・MDXの中では相対パスでOK
Markdown記法で相対パスを書けば、src/ の中の画像として最適化されます。

記事ファイルと画像を同じフォルダに置いておけるので、記事ごとに画像がまとまって管理がラクです。
MDXの中で <Image /> を使いたいときは、その記事の中で import してから渡します。
フロントマターの画像は image() ヘルパーで受ける
サムネイル(ヒーロー画像)のようにフロントマターにパスを書く画像は、コンテンツコレクションのスキーマ側で image() ヘルパーを使うと最適化の対象にできます。
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
z.object({
title: z.string(),
heroImage: image().optional(),
}),
});
schema をオブジェクトではなく関数で書き、引数から image を受け取るのがポイントです。
なお z は astro/zod からインポートしています。v5時代のサンプルでよく見る astro:content からのまとめて取得は、v6以降は非推奨になりました(astro/zod はv5でもそのまま動きます)。このあたりはコンテンツコレクション側の話なので、上のリンクカードの記事で詳しく書いています。
こうしておくと、フロントマターの heroImage: "./cover.png" が文字列ではなく ImageMetadata として渡ってくるので、そのまま <Image src={post.data.heroImage} /> に流せます。存在しないファイル名を書いた場合は、ビルドで落ちて教えてくれます。
パスが変数になる画像は import.meta.glob で読む
つまずきやすいのが、画像のパスが実行時にしか決まらないケースです。
import はビルド時に解決される仕組みなので、import img from imagePath のように変数を渡すことはできません。
この場合は、公式レシピどおり import.meta.glob でまとめて読み込み、キーで引きます。
---
import type { ImageMetadata } from "astro";
import { Image } from "astro:assets";
interface Props {
imagePath: string;
altText: string;
}
const { imagePath, altText } = Astro.props;
const images = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/*.{jpeg,jpg,png,gif}",
);
if (!images[imagePath]) {
throw new Error(`"${imagePath}" が glob に見つかりません`);
}
---
<Image src={images[imagePath]()} alt={altText} />
import.meta.glob は、パスをキー・importする関数を値とするオブジェクトを返します。
images[imagePath]() と呼び出したタイミングで、その画像が読み込まれる流れです。
glob に渡すパターンは静的な文字列で書く必要があります。ここを変数にしてしまうと何も拾えないので注意してください。

見つからなかったときにエラーを投げておくと、あとで原因を探さずに済むね!
画像まわりでつまずきやすい4つのポイント
1. public/ に置いたまま「軽くならない」と悩む
いちばん多いパターンだと思います。public/ は最適化されない場所なので、そこに置いている限り何をしても変わりません。
私はこのブログでは、記事の画像は記事フォルダに同居させて相対パスで参照する形に寄せています。public/ に置いているのは、faviconやOGP画像・吹き出しに使う人物イラストのように、URLを固定したいものだけです。
「この画像だけ重い」というときは、まず置き場所を確認してみてください。
2. リモート画像は許可した先だけ最適化される
外部URLの画像は、設定で許可したドメインのものだけが最適化の対象になります。
// astro.config.mjs
export default defineConfig({
image: {
domains: ["astro.build"],
remotePatterns: [{ protocol: "https", hostname: "**.amazonaws.com" }],
},
});
ドメインを列挙するなら image.domains、ワイルドカードでまとめたいなら image.remotePatterns です。どちらも既定値は空の配列なので、設定しないと最適化されません。
許可していないリモート画像も表示自体はされますが、加工されずそのまま読み込まれます。
3. レスポンシブ画像は layout を指定して初めて効く
画面幅に応じて srcset と sizes を自動生成する機能は、layout を指定したときだけ動きます。
<Image src={myImage} alt="デスクの写真" layout="constrained" width={800} height={600} />
指定できるのは次の4つです。
| layout | ふるまい | 向いている場所 |
|---|---|---|
constrained | 縮むが、元のサイズ以上には広がらない | 本文中の画像 |
full-width | コンテナ幅いっぱいに広がる | ヒーロー画像 |
fixed | サイズ固定。高解像度向けだけを生成 | ロゴ・アイコン |
none | 自動生成しない | 個別に無効化したいとき |
サイト全体の既定値は astro.config.mjs の image.layout で決められます。
あわせて image.responsiveStyles を true にすると、リサイズ用のCSSまで入れてくれます。既定は false なので、自前でスタイルを当てていないなら有効にしておくとよさそうです。
4. v6以降は元画像より大きく引き伸ばさない
Astro v5系までは、元画像より大きいサイズを指定すると引き伸ばして出力していました。v6.0からはこの拡大がなくなり、元のサイズが上限になっています。
同じくv6.0から、fit を指定していなくても切り抜きが効くように変わりました。
バージョンを上げたあとで「画像の見え方が変わった」と感じたら、この2点を疑ってみてください。
まとめ|「src に置いて Image で出す」が基本形
Astroの画像最適化は、難しい設定を積み上げるものではなく、置き場所と書き方を選ぶだけの仕組みです。
src/ に置いて import し、<Image /> に渡す。
この形さえ守っておけば、リサイズも形式変換もCLS対策も自動でついてきます。
- 最適化されるのは
src/に置いた画像だけ。public/は加工せずそのまま配られる src/の画像はimportして渡す。public/はwidthとheightが必須、リモートはinferSizeも使える- 複数形式を出し分けたいときは
<Picture />のformats(既定はwebpのみ) - フロントマターの画像は、スキーマを関数にして
image()ヘルパーで受ける - パスが変数になる画像は
import.meta.globでまとめて読み、キーで引く - レスポンシブ化には
layoutの指定が必要。リモート画像はimage.domainsなどの許可が必要
らるじゅまずは手元の public/ を覗いて、記事の画像が紛れ込んでいないかだけ見てみてください。
そこを src/ に移すだけで、体感が変わることも多いですよ。
