扱うものが静的サイト中心なこともあって、フレームワークの検討をする際に個人的に気になるのが画像の最適化です。
「静的生成にはAstroがいい」という話をよく見かけるようになったもので、Astroを試し始めたのですが、画像まわりの情報がなかなか見つからない。Showcaseで紹介されているサイトを確認してみても、画像の最適化はかなりゆるめで、レスポンシブイメージになっていないものも結構多い…。
どういうことなんだろうか? ということで、ちょっと調べてみました。
Astroではインテグレーションをインストールすることで機能を拡張します。画像の最適化に関しては、現状では公式が開発している@astrojs/imageと、サードパーディ製のAstro ImageToolsの二択という感じになっています。
残念ながら共存させるのは難しく、どちらかを選ぶ必要があります。
では、どちらを選ぶか?ですが、個人的な結論を言ってしまえば、「先のことは考えずに現時点での機能を評価するならAstro ImageTools。今後のことを考えるなら、@astrojs/image」という感じでしょうか。
https://github.com/RafidMuhymin/astro-imagetools
非常に多機能なインテグレーションで、ドキュメントも整っています。
<img>の代替となる<Image>に加えて、レスポンシブイメージには<Picture>コンポーネントが用意されています(さらに、<BackgroundImage />や<BackgroundPicture />といったコンポーネントも)。マルチフォーマットはもちろん、CLS対応、プレースホルダーまで整っており、Gatsby Imageやnext/legacy/imageの感覚で使うことができます。
プレースホルダーに関しては、非常に細かいチューニングまで可能になっていますし、アートディレクションなどにも対応しており、レスポンシブイメージで必要な機能はすべて用意したという感じです。
astro-imagetools.config.mjsを用意することで、こんな感じにデフォルトの設定も可能です。
import { defineConfig } from "astro-imagetools/config"
export default defineConfig({
breakpoints: [640, 750, 828, 1080, 1200, 1920, 2048],
sizes: "(min-width: 1152px) 1152px, 100vw",
format: ["webp", "jpeg"],
placeholder: "blurred",
})
また、Astroでは「.md」なファイルをそのままページとして扱うことができるわけですが、.md内の画像も自動的に最適化できる機能が用意されています(ただし、現在は機能していない模様)。
また、APIとしては、コンポーネントが生成するコードと同等のものを出力する関数が用意されており、こんな形で使うことを想定しているようです。
---
const { link, style, img } = await renderImg({
src: "/src/image/lemons.jpg",
alt: "lemons",
})
---
<Fragment set:html={link + style + img} />
非常に整ったインテグレーションですから、普通にこちらを選びたくなりますね…。
https://docs.astro.build/en/guides/integrations-guide/image/
公式が開発している画像最適化のためのインテグレーションです。<img>の代替となる<Image>に加えて、レスポンシブイメージ用の<Picture>コンポーネントが用意されており、<picture>によるマルチフォーマットに対応したコードを出力してくれます。
ただし、Astro ImageToolsの<Picture>コンポーネントとの互換性はありません。さらに、CLSの対策も一切ありません。つまり、<picture>内の<img>にwidth & height属性をつけることもありませんし、パディングを使った対応もありません。
@astrojs/image@0.11.0以前では、<picture>内の<img>に属性を追加する方法も用意されていなかったため、CLS対策はラップした要素を使って対応するしかありませんでした。@astrojs/image@0.11.0から属性をスルーしてくれるようになり、<img>で対応できるようになりましたが、width & height属性は自分で追加する必要があります。
もちろん、プレースホルダーなどもありません。
さらに、デフォルトの設定も現時点ではなく、コンポーネントにして対応するようにと案内されています。
https://docs.astro.build/en/guides/images/#setting-default-values
このあたりまで来ると、@astrojs/imageの考え方が見えてくるような気がします。
next/imageがCLSやプレースホルダーまで整えた簡単に使えるコンポーネントであったものの、結局、素に限りなく近いnext/future/imageの方が喜ばれていることを考えれば、この選択というのもありなのかもしれません(将来、もっと多機能になる可能性はありますが)。
また、このあたりの考え方はAPIの方にも見えてきます。
getPictureという関数が用意されていますが、こちらは、以下のようなオブジェクトを出力するだけです
{
sources: [
{
type: 'image/webp',
srcset: '/assets/cat_ZoRz1u.webp 640w,/assets/cat_zYpF9.webp 750w,/assets/cat_Z23obAB.webp 828w,/assets/cat_2rruqi.webp 1080w,/assets/cat_jvDtV.webp 1200w,/assets/cat_9yFwU.webp 1920w,/assets/cat_Z2mNlVJ.webp 2048w'
},
{
type: 'image/jpeg',
srcset: '/assets/cat_ZtutpC.jpeg 640w,/assets/cat_vmvh1.jpeg 750w,/assets/cat_Z2815YJ.jpeg 828w,/assets/cat_6blae.jpeg 1080w,/assets/cat_Z21JuL8.jpeg 1200w,/assets/cat_Z2bGsI9.jpeg 1920w,/assets/cat_m7CB8.jpeg 2048w'
},
{
type: 'image/jpeg',
srcset: '/assets/cat_1THka7.jpg 640w,/assets/cat_Z2aBNWb.jpg 750w,/assets/cat_gbHA0.jpg 828w,/assets/cat_Z1RhBVU.jpg 1080w,/assets/cat_14XEUE.jpg 1200w,/assets/cat_U1GXD.jpg 1920w,/assets/cat_Z1Blkv1.jpg 2048w'
}
],
image: {
fit: undefined,
position: undefined,
background: undefined,
width: 2048,
height: 727,
src: '/assets/cat_2gtsOH.jpg'
}
}
このまま機能するわけではありませんが、取得したHTML文字列内の<img>をレスポンシブイメージに変換するような処理をする場合には、こちらのほうが便利です。
(rehypePluginsに差し込むプラグインにしようと思ったものの、そちらではこのAPIを動かせなかったもので、この形にしてみました)
import { unified } from "unified"
import rehypeParse from "rehype-parse"
import rehypeStringify from "rehype-stringify"
import { visit } from "unist-util-visit"
import { h } from "hastscript"
import { getPicture } from "@astrojs/image"
import { getPlaiceholder } from "plaiceholder"
const pictureDefault = {
widths: [640, 750, 828, 1080, 1200, 1920, 2048],
sizes: "(min-width: 1152px) 1152px, 100vw",
formats: ["webp", "jpeg"],
}
export default async function imgToPicture(html) {
let imageNodesSet = []
const htmlAst = unified().use(rehypeParse, { fragment: true }).parse(html)
visit(htmlAst, "element", (node, index, parent) => {
if (node.tagName === "img") {
imageNodesSet.push({ node, index, parent })
}
})
const promises = imageNodesSet.map(async (nodeSet) => {
const {
node: {
properties: { src, alt },
},
index,
parent,
} = nodeSet
const { base64, img } = await getPlaiceholder(src)
const pictureData = await getPicture({
...img,
...pictureDefault,
aspectRatio: `${img.width}:${img.height}`,
})
// 置換用のnodeの作成
const srcNodes = pictureData.sources.map((source) => {
const e = h("source", { ...source })
return e
})
const imgNode = h("img", { ...pictureData.image, alt })
const pictureNode = h("figure", [h("picture", [...srcNodes, imgNode])])
// 置換
parent.children.splice(index, 1, pictureNode)
})
await Promise.all(promises)
const newhtml = unified().use(rehypeStringify).stringify(htmlAst)
return newhtml
}
Astro ImageToolsと比べるとずいぶんとシンプルな構成ですが、十分な機能は揃っています。
そして、インテグレーションの選択でちょっと頭が痛いのが、ソースのパスの違いです。
まず、Astroでの画像の扱いを確認しておきましょう。
.astroでは、以下のような扱いになります。
https://docs.astro.build/en/guides/images/#in-astro-files より
---
import rocket from '../images/rocket.svg';
---
<!-- Remote image on another server -->
<img src="https://astro.build/assets/logo.png" width="25" alt="Astro">
<!-- Local image stored at public/assets/stars.png -->
<img src="/assets/stars.png" alt="A starry night sky.">
<!-- Local image stored at src/images/rocket.svg -->
<img src={rocket} alt="A rocketship in space."/>
簡単にまとめると、
ということになります。画像をimportすると、プロジェクトのルートを起点としたパスに変換されます。そのため、プロジェクトのルートを起点としたパスで直接指定することもできます。
import lemons from "../image/lemons.jpg"
# console.log(lemon)
# /src/image/lemons.jpg
さらに、マークダウンファイルでは、以下のとおりです。
https://docs.astro.build/en/guides/images/#in-markdown-files より
# My Markdown Page
<!-- Local image stored at public/assets/stars.png -->
![A starry night sky.](/assets/stars.png)
<img src="/assets/stars.png" alt="A starry night sky.">
<!-- Remote image on another server -->
![Astro](https://astro.build/assets/logo.png)
<img src="https://astro.build/assets/logo.png" width="25" alt="Astro">
こちらは、リモート、ローカルともURLということになります。
@astrojs/imageはAstroに準じる形になっています(インラインでimportできるようにもなっています)。
https://docs.astro.build/ja/guides/images/#image-
ただし、@astrojs/imageをインストールした段階で、画像をimportした際に次のようなオブジェクトが返ってくるようになりますので、注意が必要です。
{
src: '/@astroimage/images/lemons.jpg',
width: 1920,
height: 1440,
format: 'jpg'
}
Astro ImageToolsは、.astroファイルに関してはimportを使わず、プロジェクトのルートを起点としたパスで指定することになりますが、リモートファイルに関してはAstroの標準と変わりありません。
https://astro-imagetools-docs.vercel.app/en/components/Picture#src より
<!-- Local Image -->
<Picture
src="/src/images/image.jpg"
alt="A local image"
/>
<!-- Remote URL -->
<Picture
src="https://example.com/image.jpg"
alt="A remote image"
/>
マークダウンファイルに関しては以下のとおりです。マークダウンファイルか、プロジェクトのルートを基準にしたパスで指定できますので、Astro標準よりも使いやすい構成になっています。
(ただし、現在では、.mdでのコンポーネントの使用は非推奨になっています)
https://astro-imagetools-docs.vercel.app/en/markdown-images#example-markdown-images-usage より
---
src: https://picsum.photos/1024/768
alt: A random image
setup: |
import { Picture } from "astro-imagetools/components";
---
# Hello Markdown Images
<!-- A remote image -->
![A random remote image](https://picsum.photos/1024/768)
<!-- A local image relative to the markdown file -->
![A local image](./images/landscape.jpg)
<!-- A local image relative to the project root -->
![Another local image](../src/images/landscape.jpg)
<!-- An example of using query params -->
![A remote image with query params](https://picsum.photos/1024/768?grayscale)
<!-- An example of the `<Image />` component inside MD pages -->
<Picture
src={frontmatter.src}
alt={frontmatter.alt}
/>
.md内の画像処理は現状では機能しないようです。このあたりはAstroの仕様変更によるものといった書き込みを見かけますが、どのような扱いになるのか気になるところです。
画像をimportした際の処理もAstro ImageToolsは大きく異なります。最適化後の画像のパスが返ってきますので、この点も注意が必要です。
https://astro-imagetools-docs.vercel.app/en/usage#plugin-usage
import lemons from "../image/lemons.jpg"
# console.log(lemon)
# /assets/lemons@1920w.4f35acc8.jpeg
インテグレーションを切り替えなければならなくなった場合には、なかなか面倒なことになりそうです。
このような状況なため、最初の選択が重要だと思います。画像の管理をどうするのか、コンテンツの主体は何なのか、じっくりと検討する必要があるでしょう。
どちらのインテグレーションも、正式リリース前ですので、まだまだ変わる可能性はありますので、なかなか頭の痛いポイントです。