Vercel ISRページサイズ超過エラーの解決方法
Next.jsアプリケーションをVercelにデプロイする際、ISR(Incremental Static Regeneration)ページのサイズが制限を超えてビルドエラーが発生する問題と、その解決方法を紹介します。
使用技術
本記事で扱うプロジェクトの主要な技術スタックは以下の通りです。
- Vercel
- Next.js
- React
- TypeScript
- pnpm
- MDX: Markdown + JSX によるコンテンツ管理
- next-mdx-remote: MDX コンテンツのシリアライズとレンダリング
- Sharp: 画像の最適化・変換
アーキテクチャ
今回問題が発生したプロジェクトは、以下のような構造で管理されていました。
posts/
post_01/
index.ja.mdx # 記事本文
assets/ # 画像ファイル
main.jpg
images01.jpg
...
- 記事は
posts/[slug]/index.[locale].mdxとして保存 - ビルド時に
getStaticPropsで記事を読み込み - MDX コンテンツを
serialize()でシリアライズ - 関連記事のメタデータも同時に取得
- 画像はビルド時に Sharp で WebP 変換・リサイズ
問題の概要
発生したエラー
Error: Oversized Incremental Static Regeneration (ISR) page:
_next/data/XXXXXX__XXXXXXXXXXX-X/ja/posts/post_01.json.fallback (32.64 MB).
Pre-rendered responses that are larger than 19.07 MB result in a failure
(FALLBACK_BODY_TOO_LARGE) at runtime.
Vercel では ISR ページのサイズ制限が 19.07MB となっており、これを超えるとビルドエラーが発生します。今回のケースでは、特定のブログ記事ページ(post_01)が制限を大幅に超過していました。
原因の特定
初期仮説:画像ファイルのBase64エンコード
最初は、(そんなことはないだろうと思いつつも...)記事に含まれる画像が Base64 エンコードされて HTML に埋め込まれているのではないかと考えました。
しかし、元の mdx ファイルを確認したところ以下の通りでした。
ls -lh posts/post_01/index.ja.mdx
# -rw-r--r-- 12K index.ja.mdx
wc -l posts/post_01/index.ja.mdx
# 104 linesmdx ファイル自体はわずか 12KB、104 行でした。したがって、画像の Base64 エンコードが原因ではありませんでした。
画像ファイルの確認
次に、関連する画像ファイルを確認しました。
du -sh posts/post_01/
# 3.8M
ls -lh posts/post_01/assets/
# main.jpg 711K
# images01.jpg 337K
# images02.jpg 348K
# ... (合計11枚、約3.8MB)画像は合計 3.8MB でしたが、ビルド後のファイルは 33MB に膨れ上がっていました。
ls -lh .next/server/pages/ja/posts/post_01.json
# -rw-r--r-- 33M post_01.json真の原因:関連記事のネスト
コードを調査した結果、問題の本質が明らかになりました。
記事には「関連記事」機能があり、post_01 には 4 つの関連記事(post_02, post_03, post_04, post_05)が紐付いていました。
問題のコードは関連記事を取得している関数でした。
const relatesFactory = ({
locale,
relates = [],
}: WithLocale<Pick<PostMetaDto, 'relates'>>) =>
Promise.all(
relates.map(async relate => {
const file = listFiles(join(POSTS_PATH, relate)).find(
equals(`index.${locale}.mdx`)
)
// 問題:parseMarkdownで完全なコンテンツを取得している
const { meta } = await parseMarkdown({
locale,
slug: relate,
path: join(POSTS_PATH, relate, file),
})
...この実装には 2 つの問題がありました。
- 不要なコンテンツのシリアライズ:
parseMarkdown内でserialize(content)が実行され、関連記事の完全な MDX コンテンツがシリアライズされているが、実際にはmetaしか使用していない - 再帰的なネスト:
parseMarkdown→factory→relatesFactoryと再帰的に呼ばれ、関連記事の関連記事も含まれていた
つまり、post_01 → 4 つの関連記事 → それぞれの関連記事 → さらにその関連記事... というように、データが指数関数的に膨れ上がっていました。
解決方法
修正内容
relatesFactory 関数を修正し、メタデータのみを取得するように変更しました。
const relatesFactory = ({
locale,
relates = [],
}: WithLocale<Pick<PostMetaDto, 'relates'>>) =>
Promise.all(
relates.map(async relate => {
const file = listFiles(join(POSTS_PATH, relate)).find(
equals(`index.${locale}.mdx`)
)
// 修正:parseMarkdownを使わず、メタデータだけを読み取る
const input = readFileSync(join(POSTS_PATH, relate, file))
const { data } = matter(input)
...変更のポイント
parseMarkdownを呼ばない:matterで直接フロントマターからメタデータだけを読み取るserializeを実行しない: MDX コンテンツのシリアライズをスキップ- 再帰を防ぐ: 関連記事の関連記事を含めない
結果
修正後はビルドが正常に完了し、生成される JSON ファイルのサイズも数百 KB 程度に削減されました。
デバッグ方法
1:生成ファイルの直接確認
ローカルでビルド後のフォールバックファイルを確認することも有効です。 今回の例ではエラーログを確認した時点で json が問題であると分かっていたため、こちらの方法で問題の原因を特定しました。
# ビルド実行
pnpm run build
# 生成されたファイルの場所を確認
ls -lh .next/server/pages/
# 特定のページのJSONを確認
cat .next/server/pages/ja/posts/post_01.json | head -n 100JSON ファイルの内容を見ることで、どのようなデータが含まれているかを直接確認できます。
2:Bundle Analyzerの活用
問題の原因を特定する際に、Next.js Bundle Analyzer が非常に有効です。
セットアップ
pnpm install --save-dev @next/bundle-analyzernext.config.js に設定を追加:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// 既存の設定
reactStrictMode: true,
// ...
})
分析の実行
ANALYZE=true pnpm run buildこれにより、ビルド後にブラウザが自動的に開き、バンドルの視覚化されたマップが表示されます。どのファイルやモジュールがサイズを占めているかが分かります。
追加の最適化提案
画像の事前圧縮
今回の主な原因ではありませんでしたが、ビルド時間の短縮とコスト削減のため、画像の事前圧縮も推奨されます。
# ImageMagickを使用した圧縮例
cd posts/post_01/assets
mogrify -quality 85 -strip *.jpg推奨サイズ
モバイル利用も考慮すると、1 ページあたり 1.6MB 以内が理想とされています。 記事内の挿入画像は 1 枚あたり 200KB 以下を目指すのが良いですが、記事の性質や画質とのバランスも考慮してください。
今回のプロジェクトでは buildAssets 関数で Sharp を使用して画像を変換していますが、元画像が大きいほど処理時間がかかります。
事前に圧縮しておくことで、この処理を高速化できます。
学んだこと
- ファイルサイズの見た目に騙されない: mdx ファイルが小さくても、ビルド時に膨大なデータが生成される可能性がある
- データの取得は必要最小限に: 表示に使わないデータは取得しない
- 再帰的な処理に注意: 関連データを取得する際は、無限ループやデータの肥大化に注意
- ビルド後のファイルを確認:
.nextディレクトリ内の生成ファイルを確認することで、問題の原因を特定できる
まとめ
Vercel ISR ページのサイズ超過エラーは、一見すると画像やコンテンツのサイズが原因に見えますが、実際にはデータ取得のロジックに問題があることがあります。
今回のケースでは、関連記事を取得する際に不要なコンテンツまでシリアライズしていたことが原因でした。 データ取得は必要最小限にとどめ、ビルド後のファイルサイズを定期的に確認することが重要です。