ビルド30分→3分に。Next.jsをやめてAstroに移行した全記録

こんにちは。ROXX Zキャリアプロダクト開発部、エンジニアの平です。

今回は、弊社の就職・転職ガイドメディアを Next.js Page Router から Astro + Cloudflare Workers へ移行し、ビルド時間を30分以上 → 3分、表示速度を大幅に改善した話をお届けします。

「記事を公開してから30分待つって何...?」と思われるかもしれませんが、これが私たちの現実でした。この記事では、どのようにこの問題を解決し、SEO パフォーマンスを大幅に改善したのかを詳しく書いていきます。

目次

  1. どうしてこうなった? 背景と課題
  2. Next.js を捨てる決断をするまで
  3. ISR と R2 でビルドレス実現
  4. Turborepo でモノレポを整理
  5. SEO 最適化の取り組み
  6. 移行の成果と学び

どうしてこうなった? 背景と課題

サービスの成長と共に現れた問題

弊社の就職・転職ガイドは、Next.js Page Router + AWS Lambda という構成で運用していました。 最初は快適でした。記事数も少なく、ビルドも数分で終わっていました。

でも、サービスが成長するにつれて...

🔥 問題1: ビルド時間が30分超え

これが一番つらかったです。

microCMS をヘッドレス CMS として使っており、記事更新時には手動で全ページを SSG (Static Site Generation) で再ビルドする方式を採用していました。

ビルド時間の変遷:

  • 記事数 100本: ビルド時間 3分 → 快適 ✅
  • 記事数 500本: ビルド時間 10分 → まあ許容範囲
  • 記事数 1,000本: ビルド時間 20分 → ちょっと厳しい
  • 記事数 1,500本: ビルド時間 30分超え → もう無理 ❌
記事更新 → ビルド開始 → 30分待機 → デプロイ完了

またよくビルドに失敗してマーケティングチームの方々が手動でGitHub Actionsを実行するとかいう運用になっていました。 どういう運用だよ!!

💸 問題2: インフラコストの増大

トラフィックが増えるにつれ、Lambda の実行コストも右肩上がりに。 サイト自体が広告費をかけずにコンバージョンを目的とするサイトにも関わらずインフラ費用は無視できません。

弊社のメイン事業であるZキャリアは、Next.js + Cloudflare Workersで動いていますが、ガンガンSSR使っているにもかかわらず激安 それに比べシンプルなSSGのサイトで何十倍ものコストを支払っている...?

📉 問題3: Performance スコアが 48点

PageSpeed Insights で計測してみたら衝撃の結果が。

PageSpeed Insights の結果(移行前):

  • Performance: 48点 ❌
  • LCP (Lighthouse): 6.4秒 ❌
  • LCP (実測値): 2.1秒 ⚠️
  • INP: 355ms ❌
  • TBT: 1,400ms ❌

メディアサイトとして、これは致命的です。検索流入が主要なトラフィック源なのに、SEO 的に不利な状態でした。

主な問題点:

  • JavaScript の実行時間: 3.2秒
  • JavaScript のサイズ: 4,699 KB(めちゃくちゃ重い)
  • メインスレッド処理時間: 5.4秒
  • 使っていない JavaScript: 122 KB
  • 最適化されていない画像: 3,479 KB 削減可能

Chakra UI (CSS-in-JS) を使っていたので、実行時のスタイル生成のオーバーヘッドも大きかったですね。 ほとんどの原因は大量のGTM(Google Tag Manager)だったことに後で気づくことになりますが、、

🔧 問題4: 技術的負債

モノレポ内の各パッケージが親の package.json に依存していて、依存関係の管理が複雑になっていました。ビルドも遅いし、新しいライブラリを追加するのも一苦労でした。 また、元々はあまりビジネス的に注力していなかった分でもあり、活発に開発が行われておらず、負債が積もっていっていました。 だけど予想以上にアクセスが急増し、このままではまずいかもという状況になっていました。

じわじわと増えるトラフィック

Next.js を捨てる決断をするまで

最初は「インフラだけ変えればいいよね」と思っていた

当初、私たちは単純に考えていました。

「コストを下げたいなら、Lambda を Cloudflare Workers に変えればいいんじゃない?」

確かに、Cloudflare Workers は Lambda よりコスト効率が良いです。でも、調査を進めるうちに、そう簡単ではないことが分かってきました。

OpenNext の壁にぶつかる

Next.js を Cloudflare Workers で動かすには、OpenNext という OSS を使うのが一般的でした。 弊社のメイン事業であるZキャリアもNext.js App Router + Cloudflare Workersで動いており、以前にLambdaから移行の経験もあったのでサクッと終わると思いきや...

「あれ、記事詳細ページが開けない?」

OpenNext の制約により、Page Routerでのダイナミックルーティングの機能が Cloudflare Workers 環境では正常に動作しませんでした。 またISRを再現するためにR2やD1を用意したり予想以上に作業時間の見積もりが膨らんでいました。

App Router への移行を検討するも...

OpenNext の問題を解決するため、App Router への移行を検討しました。しかし、新たな問題が次々と。

Chakra UI v2 (CSS-in-JS) の問題:

  • @emotion/react が Server Components と互換性がない
  • コンポーネント'use client' にする必要がある
  • コードベースを大規模に書き直す必要がある

技術的負債の継承:

  • モジュール間の密結合
  • 過剰な JavaScript バンドル
  • Page Router の設計上の問題

そして気づいたこと:

「既存の負債を引き継ぎながら App Router に移行するコストをかけるなら、いっそゼロベースで作り直した方が良くない?」

記事メディアに Next.js は必要なのか?

冷静に考えると、記事メディアサイトって以下の特徴がありますよね。

一方、Next.js は:

  • API Routes、Middleware、Image Optimization など多機能
  • JavaScript バンドルが大きい

私たちに必要なのは:

  • シンプルな SSG/ISR
  • 最小限の JavaScript
  • 最大限の SEO 最適化

「Next.js、オーバースペックじゃね?」という結論に至りました。

Astro という選択肢

そこで出てきたのが Astro でした。

Astro の特徴:

  • デフォルトで JavaScript ゼロ
  • 必要な部分だけ React などを使える(Islands Architecture)
  • 静的コンテンツの配信に特化
  • Cloudflare Workers ネイティブサポート
  • Vite ベースで開発体験が良い

比較してみた:

項目 App Router 移行 Astro 移行
移行コスト 中〜大
技術的負債の解消 △ 一部残る ◎ 完全解消
パフォーマンス改善 △ 限定的 ○ 改善
将来の保守性 △ 複雑さ継続 ◎ シンプル化
SEO 効果 ○ 可能 ◎ 最大化

解決したい課題を整理

Astro への移行で、以下の課題をまとめて解決できると判断しました。

  • ✅ ビルド時間の完全解消(ISR + R2)
  • ✅ コスト最適化(Cloudflare Workers)
  • ✅ OpenNext の制約回避(Cloudflare Workers ネイティブ)
  • Chakra UI の問題解決(CSS-in-JS 不要に)
  • SEO パフォーマンス向上(JavaScript 最小化)
  • ✅ 技術的負債の清算(ゼロベースで設計)
  • ✅ 開発体験の向上(Vite ベース)

最終的に、「段階的な改善」ではなく「根本的な解決」を選択しました。

Astro への移行作業

Chakra UI からの脱却戦略

Chakra UI を使っていたので、全部書き直す必要がありました。でも、一から作るのは大変なので、既存の弊社のデザインシステムである zing-ui(shadcn/ui ベースのデザインシステム)を活用することに。

Islands Architecture の活用

Astro の強みは Islands Architecture です。静的な部分は HTML だけ、インタラクティブな部分だけ JavaScript を配信します。

移行後の JavaScript バンドルサイズ

移行前(Next.js):

Total: 4,699 KB
├─ framework.js: 1,200 KB(React関連)
├─ main.js: 800 KB
├─ chakra-ui.js: 400 KB(CSS-in-JS)
├─ その他のライブラリ: 2,300 KB
└─ 使用していないJS: 122 KB

移行後(Astro):

Total: 400-600 KB(約87-90% 削減!)🚀
├─ client.js: 約60 KB(Astroクライアントランタイム)
├─ header.js: 約20 KB(React - ヘッダーのみ)
├─ category-list.js: 約10 KB(カテゴリアコーディオン)
├─ その他: 約20 KB
└─ 使用していないJS: わずか24 KB

改善点:
✅ Reactのhydrationは最小限(ヘッダーのみ)
✅ CSS-in-JSを廃止(Tailwind CSS v4に移行)
✅ 静的コンテンツはJavaScriptゼロ
✅ Islands Architectureで必要な部分だけJS配信

結果:JavaScript実行時間が3.2秒 → ほぼ0秒に! (後からGTM付きで計測したらこの結果になりませんでした、、😢詳しくは下記に記載しています)

ISR と R2 でビルドレス実現

アーキテクチャ

Astro + Cloudflare Workers + R2 の組み合わせで、ビルドが不要になりました。

全体アーキテクチャ:

📈 全体システム構成図

🎯 キャッシュ戦略の3段階

アーキテクチャのポイント:

  1. キャッシュ戦略

    • R2(オブジェクトストレージ): 永続(Webhook で削除)
    • カテゴリツリー(R2): 永続(Webhook で更新)
  2. ゼロダウンタイム更新

    • STALE(古いキャッシュの場合)の時、古いコンテンツを返しつつバックグラウンドで更新
    • ユーザーは常に即座にレスポンスを受け取れる
    • Webhookによるキャッシュ無効化で即座に最新コンテンツを配信
  3. API 呼び出し最適化

    • フィールド制限で 6MB → 0.5MB (90% 削減)
    • カテゴリツリーをキャッシュして API 呼び出しを劇的に削減

キャッシュ戦略の実装

キャッシュ戦略はR2を採用しました。

どういうこと?

  • 最初のアクセス: SSR 実行 → R2 にキャッシュ保存
  • 10分以内のアクセス: R2 のキャッシュをそのまま返す(超速い)
  • 10分〜1時間のアクセス: 古いキャッシュを返しつつ、バックグラウンドで更新
  • 1時間以上: キャッシュ削除 → SSR 実行

Webhookで記事更新時に即座にキャッシュを削除できるため、10分というキャッシュ時間でも問題なく運用できています。

microCMS Webhook でキャッシュパージ

記事を更新したら、該当記事のキャッシュを削除する仕組みも作りました。

Webhook APIでは、以下の処理を行います:

  1. 署名検証: microCMSからの正規のリクエストか確認
  2. キャッシュ削除: 該当記事、トップページ、カテゴリツリーをR2から削除
  3. カテゴリツリー再構築: 最新のカテゴリ構造をmicroCMSから取得してR2に保存

この仕組みにより、記事公開・更新時に即座に最新コンテンツが配信されます。

結果:

  • ビルド時間: 30分以上 → 3分
  • 記事公開リードタイム: 30分以上 → 即座に反映

編集者から感謝のメッセージが届くようになりました 🎉

Turborepo でモノレポを整理

モノレポの問題

移行前のモノレポは、各パッケージが親の package.json に依存していて、管理が大変でした。

  • 依存関係の重複インストール
  • ビルドが遅い
  • どのパッケージがどれに依存しているか分からない

Turborepo の導入

Turborepo を導入して、各パッケージを独立させました。

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", ".astro/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

各パッケージが独自の package.json を持つようになり、依存関係も明確になりました。

SEO 最適化の取り組み

画像の最適化

https://blog.microcms.io/imgix-picture/ 上記の公式記事を参考に、MicroCMSの画像最適化を行いました。 レイアウトシフトを防ぐためのプレースホルダーや、その他諸々少し工夫を入れています

効果: - フォーマット: JPEG → WebP/AVIF - LCP の大幅改善に貢献

構造化データの実装

SEO のために、構造化データ(JSON-LD)も実装しました。

<script type="application/ld+json" set:html={JSON.stringify({
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: article.title,
  description: article.description,
  image: article.thumbnail?.url,
  datePublished: article.publishedAt,
  author: {
    '@type': 'Organization',
    name: 'Z Career',
  },
})} />

Google の検索結果でリッチリザルトとして表示されるようになりました。

移行の成果と学び

数値で見る成果

Lighthouse スコア:

指標 移行前 移行後 改善率
Performance 48 73 +52% 🎉
LCP (Lighthouse) 6.4s 2.6s -59% 🎯
FCP 2.1s 1.5s -29%
INP 355ms 274ms -23%
TBT 1,400ms 980ms -30%
Speed Index 4.2s 3.0s -29%

JavaScript削減(実測値):

項目 移行前 移行後 削減率
ペイロードサイズ 4,699 KB 計測不要レベルに改善 約90%削減 🚀
JavaScript実行時間 3.2s 2.1s 約34%削減
メインスレッド処理 5.4s 3.2s 約41%削減
Total Blocking Time 1,400ms 980ms 30%削減 🎊

その他の成果:

  • ビルド時間: 30分以上 → 3分
  • 画像最適化: 3,479 KB → 指摘ゼロ
  • インフラコスト: 約 90% 削減

Core Web Vitals の評価: 不合格 → 不合格 😢

完全に解決するにはGTMをなんとかしないといけないということに気づきました (試しに無効にするとスコア98) このあたりはまたどうするか検討して解決していきたいですね

ビジネスインパク

編集者の生産性向上:

  • 記事公開リードタイム: 30分以上 → 即座
  • 緊急修正への対応: 不可能 → 即座に対応可能

学んだこと

1. フレームワーク選択は慎重に

「とりあえず Next.js」という選択は正直今でもありだと思いますが、サービスの特性に合ったフレームワークを選んだほうが良い場合もあるかも?と思いました

記事メディアにAstroは抜群に相性が良いと感じました

2. 技術的負債は早めに返済

「いつか直そう」と思っていた負債が、気づいたら巨大化していました。定期的にリファクタリングする時間を確保することが重要だと学びました。

3. ゼロベース再構築の価値

段階的な改善も大事ですが、時にはゼロベースで作り直す勇気も必要だと思いました。 AIコーディングが進化した今、積もり積もった負債を少しずつ返済するより小規模なサイトならゼロベースのほうがてっとり早いこともあるかもしれません。 CIやインフラ、コードベースの移行も含めて3日程度で作業は完了しました。 負債解消に取り組むのも大事だけど、いっそのこと負債を捨てる勇気も必要かもと思いました。

おわりに

ビルド時間 30分超え、Performance スコア 48点という状態から、ビルド時間3分、Performance スコア 70点台まで改善できました。

移行作業は大変でしたが、編集者からの感謝の声や、数値で見える改善効果を見ると、やって良かったと心から思います。

もし同じような課題を抱えているチームがあれば、ぜひ Astro を検討してみてください。記事メディアには本当に最適なフレームワークだと思います。

参考リンク

ROXXってどんな会社?気になった方はこちらから👇️

note.roxx.co.jp note.roxx.co.jp