DevToolsのLighthouseタブで、緑色の「100」が4つ並ぶ。 これはエンジニアにとって、ある種の中毒性を持つ光景です。

以前の記事でAstroへの移行とLighthouse 100点達成について書きましたが、記事数が増えるにつれて新たな課題が見え隠れし始めました。 今回は、ページネーションの実装によるスケーラビリティの確保と、LCP を極限まで縮めるための試行錯誤、そして「最適化のやりすぎ」で開発環境を破壊しかけた失敗談を共有します。

課題1:全件取得のボトルネックとページネーション

これまでのトップページ(index.astro)は、シンプルに「全記事を取得して表示」していました。 しかし、記事が増えれば増えるほど、ビルド時の画像処理やクライアントでのDOM量が増加します。Astroのアイランドアーキテクチャは優秀ですが、不要なデータを送らないに越したことはありません。

解決策:[...page].astro への移行

Astroには強力なページネーション機能が標準で備わっています。これを使うために、ファイル名を index.astro から [...page].astro に変更し、getStaticPathspaginate 関数を使用するように書き換えました。

// src/pages/[...page].astro
export async function getStaticPaths({ paginate }) {
    const posts = (await getCollection('blog')).sort(/* ... */);
    // 1ページあたり10件に制限
    return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;

これだけで、page.data に当ページ分の記事だけが格納され、page.url.prev / page.url.next で前後のページへのリンクも自動生成されます。 開発者体験として、この手軽さは驚異的です。

課題2:LCPスコアの低下と「レイジー」の弊害

ページネーションでDOM量は減りましたが、Lighthouseのパフォーマンススコアにおいて LCP が警告を出していました。

原因は、すべての画像に対して「善意で」付与していた loading="lazy" でした。 ファーストビューに入る一番上の記事(ヒーロー画像)まで遅延読み込みされてしまい、ブラウザが「あ、これも読み込むのね」と気づくのが遅れていたのです。

解決策:条件付きの Eager Loading

トップページのリストにおいて、「1枚目の画像だけは即座に読み込み、2枚目以降は遅延させる」 というロジックを実装しました。

// 1枚目(index === 0)だけ eager / high priority に設定
<PostCard 
    post={post} 
    loading={index === 0 ? "eager" : "lazy"}
    fetchpriority={index === 0 ? "high" : "auto"}
/>

また、個別記事ページ(blog/[...slug].astro)のヒーロー画像においても、ネイティブの <img> タグではなく Astroの <Image /> コンポーネントを使用しつつ、loading="eager"fetchpriority="high" を明示しました。

失敗談:最適化の「やりすぎ」によるTTFB悪化

LCPをさらに短縮しようとして、「サーバーサイドで getImage を使って画像のURLを計算し、<head> タグ内で <link rel="preload"> する」 という実装も試みました。

理論上は最速のはずですが、これが開発環境(pnpm run dev)で牙を剥きました。 各リクエストのたびに同期的に画像処理が走ってしまい、TTFBが12秒近くまで悪化してしまったのです。さらに、Astroのコンパイラがクラッシュする事態にも見舞われました。

教訓: Astroの <Image /> コンポーネントが出力する最適化だけで十分強力です。開発サーバーの挙動をブロックしてまで、手動でプリロードをねじ込む必要はありませんでした。SSG(静的サイト生成)ならではの落とし穴といえます。

また、そもそもパフォーマンス計測を pnpm run dev(開発サーバー)で行っていたのが混乱の原因でした。開発サーバーは最適化されていないため、本番相当の計測をする際は必ず pnpm run build && pnpm run preview で確認するべきですね。基本を忘れてはいけません。

仕上げ:アクセシビリティの階層構造修正

最後に、Accessibilityスコアにおける「Heading levels should only skip by one(見出しレベルは1つずつ下がるべき)」という指摘を修正しました。

  • h1: サイトタイトル
  • h3: 記事タイトル(以前の状態)

となっており、h2 が欠落していました。これを以下のように修正しました。

  • h1: サイトタイトル
  • h2: 記事タイトル(記事一覧における各カードのタイトル)

また、もう一点アクセシビリティで指摘を受けたのが、コードハイライトのコントラスト比です。 私は好んで「Dracula」テーマを使っていますが、デフォルトのコメント色(#6272A4)は背景色とのコントラストが低く、アクセシビリティ監査に引っかかります。

ここでテーマごと変更するのは簡単ですが、こだわりは捨てたくありませんでした。そこで、以下のCSSハックを追加し、「コメント部分だけ強制的にコントラスト比の高い色(Cool Gray 400)にする」という対策を行いました。

/* Accessibility Hack: Force higher contrast for comments in Dracula theme */
pre.astro-code span[style*="color:#6272A4"] {
    color: #A0AEC0 !important; /* Cool Gray 400 */
}

些細なことですが、スクリーンリーダー利用者にとっても、SEOにとっても、「正しいHTML」を書くことはエンジニアの規律です。

結果:再びの満点

これらの修正を経て、ビルド環境でのLighthouseスコアは再び オールグリーン、満点 を達成しました。

  • Performance: 100
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100

単に数字を追うだけでなく、読み手にとっても、書き手にとっても快適なブログ環境が整いました。 AIエージェントと共にコードを書くことで、こうした細かい最適化のサイクルを高速に回せるのは、現代のエンジニアリングの醍醐味と言えるでしょう。