32blog.comをVercelに本番デプロイするとき、ローカルでは npm run build が通るのに、Vercelに上げた瞬間にビルドが死ぬ——そういう経験を何度もしてきた。
「ローカルでは動く」は「Vercelでも動く」を意味しない。環境変数、Node.jsバージョン、キャッシュの扱い、Function制限……ローカルとVercelの環境差異は思ったより大きい。
この記事では、Next.jsをVercelにデプロイする際によく踏む7つのエラーパターンと、それぞれの確実な解決策を解説する。エラーメッセージを見つけたらCtrl+Fで検索してほしい。
ビルドがローカルでは通るのにVercelで失敗する
最も頻繁に遭遇するパターンだ。原因のほとんどは「環境変数の未設定」か「依存関係の解決失敗」のどちらかに絞られる。
環境変数が設定されていない
ローカルでは .env.local に書いた値が自動で読まれる。Vercelでは環境変数をダッシュボードから明示的に設定しないと読まれない。
ビルド中に環境変数が必要なコードが実行されると、こんなエラーになる。
Error: Missing required environment variable: DATABASE_URL
Build failed with errors
または、環境変数が undefined のまま処理が進んでクラッシュするパターンもある。
TypeError: Cannot read properties of undefined (reading 'split')
解決策: Vercelダッシュボード → Project → Settings → Environment Variables に追加する。
# ローカルの .env.local を確認
cat .env.local
# 以下のような変数がある場合はすべてVercelに追加が必要
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
AUTH_SECRET=...
環境変数を設定した後は「Redeploy」ではなく、新しいコミットをプッシュするか「Redeploy without cache」を選ぶと確実にビルドが通る。
TypeScriptの型エラー
ローカルでは npm run dev が --turbopack で起動していて型チェックをスキップしているが、Vercelのビルドは next build を実行するため型エラーで止まる。
Type error: Property 'name' does not exist on type 'User | null'
解決策: デプロイ前にローカルで必ず型チェックを走らせる習慣をつける。
# ビルド前に必ず実行する
npx tsc --noEmit
# 型エラーを修正してからプッシュ
next.config.ts で型チェックを無効化する方法もあるが、本番コードの品質が下がるので非推奨だ。
// next.config.ts(非推奨: 型チェックを無効化する設定)
const nextConfig = {
typescript: {
ignoreBuildErrors: true, // やむを得ない場合のみ
},
};
ESLintエラー
next build はESLintも実行する。npm run dev では引っかからなかったESLintエラーがビルド時に出ることがある。
./app/components/Button.tsx
ESLint: 'onClick' is defined but never used. (no-unused-vars)
Failed to compile.
解決策: ローカルで事前にLintを通す。
# ビルド前にLintを確認
npm run lint
# 自動修正できるものは修正
npm run lint -- --fix
急ぎの場合は next.config.ts でLintを一時的に無効化できるが、技術的負債になるので必ず後で修正する。
// next.config.ts
const nextConfig = {
eslint: {
ignoreDuringBuilds: true, // 一時対応のみ
},
};
Function Timeoutエラーが出る
Vercelの Serverless Functions にはタイムアウト上限がある。デフォルトは10秒(Hobbyプラン)、Proプランでも最大300秒(5分)だ。
Error: Task timed out after 10.00 seconds
原因と確認方法
Vercelダッシュボードの Functions タブを開くと、各関数の実行時間が確認できる。10秒に迫っているものがあれば要対応だ。
よくある原因は以下の通り。
- 外部APIへのリクエストが遅い
- データベースのクエリが最適化されていない
- 大きなファイルをメモリ上で処理している
- 無限ループや意図しない再帰
解決策1: maxDurationを設定する(Proプラン)
// app/api/heavy-task/route.ts
export const maxDuration = 60; // 最大60秒(Proプランのみ)
export async function GET() {
// 重い処理
const result = await heavyDatabaseQuery();
return Response.json(result);
}
解決策2: 処理を非同期キューに逃がす
重い処理はAPIルートで同期的に実行せず、バックグラウンドジョブに委ねる。Vercelなら Vercel Cron Jobs や外部サービス(Upstash QStash など)を使う方法がある。
// app/api/trigger-job/route.ts
export async function POST(request: Request) {
const body = await request.json();
// 重い処理を非同期キューに投げる
await qstash.publishJSON({
url: `${process.env.VERCEL_URL}/api/process-job`,
body: body,
});
// すぐにレスポンスを返す
return Response.json({ status: "queued" });
}
解決策3: Edge Runtimeを使う
軽量な処理なら Edge Runtime に切り替えるとコールドスタートが速くなり、タイムアウトも起きにくい。
// app/api/fast-api/route.ts
export const runtime = "edge";
export async function GET() {
// Edge Runtimeはコールドスタートが速い
return new Response("Hello from Edge!");
}
メモリ不足(OOM)エラー
ビルド中または実行中にメモリが足りなくなると、このエラーが出る。
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
または Vercel のログに以下のように記録される。
Error: Function exceeded memory limit of 1024 MB
ビルド時のメモリ不足
大規模なプロジェクトや、ビルド時に多くのページを静的生成する場合に起きやすい。
# Node.jsのメモリ制限を増やしてビルド
NODE_OPTIONS="--max-old-space-size=4096" npm run build
Vercelのビルド設定でも同様に環境変数を追加できる。
# Vercelダッシュボード > Environment Variables
NODE_OPTIONS = --max-old-space-size=4096
実行時のメモリ不足
APIルートで大きなデータをメモリ上に展開している場合に発生する。
// ❌ 悪い例: 大きなJSONを一度にメモリに展開
export async function GET() {
const hugeData = await fetchAllRecords(); // 100MB のデータ
return Response.json(hugeData);
}
// ✅ 良い例: ストリーミングレスポンスを使う
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const records = await fetchRecordsInBatches();
for await (const batch of records) {
controller.enqueue(
new TextEncoder().encode(JSON.stringify(batch) + "\n")
);
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "application/json" },
});
}
404エラーが本番環境だけで出る
ローカルでは正常なのに、Vercelにデプロイすると特定のページが404になるパターンがある。
動的ルートの静的生成漏れ
// app/blog/[slug]/page.tsx
// generateStaticParamsが不完全だと本番でのみ404になる
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
generateStaticParams が返すスラッグ以外のパスにアクセスすると404になる。dynamicParams を true(デフォルト)にすると、未生成のパスにもアクセスできるようになる。
// app/blog/[slug]/page.tsx
// dynamicParams のデフォルトは true なので、未生成パスもオンデマンドで生成される
export const dynamicParams = true;
// dynamicParams = false にすると生成されていないパスは明示的に404になる
next-intl のルーティング設定ミス
多言語対応している場合、next-intl のロケール設定が本番環境と開発環境でズレることがある。
// middleware.ts
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
locales: ["ja", "en"],
defaultLocale: "ja",
// localePrefix: "always" の場合、/ja/... が必須になる
localePrefix: "as-needed", // デフォルトロケールはプレフィックスなし
});
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
キャッシュが古い状態で固まる
Vercelのキャッシュ戦略が予期しない動作を引き起こすことがある。特に fetch のキャッシュとISR(Incremental Static Regeneration)の組み合わせで起きやすい。
// ❌ キャッシュ戦略を明示していない
export async function getArticles() {
const res = await fetch("https://api.example.com/articles");
// Next.js 15+ではデフォルトno-store。明示しないと意図が不明確
return res.json();
}
// ✅ 適切なキャッシュ戦略を明示する
export async function getArticles() {
const res = await fetch("https://api.example.com/articles", {
// 60秒ごとに再検証
next: { revalidate: 60 },
});
return res.json();
}
// または完全にキャッシュを無効化
export async function getLatestPrice() {
const res = await fetch("https://api.example.com/price", {
cache: "no-store",
});
return res.json();
}
On-Demand Revalidationを使う
コンテンツ更新があった際に即座にキャッシュを破棄したい場合は、Revalidation APIを使う。
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token");
// 不正なリクエストを拒否
if (token !== process.env.REVALIDATION_TOKEN) {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
const body = await request.json();
if (body.path) {
revalidatePath(body.path);
}
if (body.tag) {
revalidateTag(body.tag);
}
return Response.json({ revalidated: true });
}
デプロイは成功するがCORSエラーが出る
フロントエンドからAPIを叩いたとき、本番環境だけCORSエラーが出るケースがある。
Access to fetch at 'https://api.example.com' from origin 'https://32blog.com'
has been blocked by CORS policy
Next.jsのAPIルートにCORSヘッダーを追加することで解決できる。
// app/api/data/route.ts
export async function GET(request: Request) {
const response = await fetchData();
return Response.json(response, {
headers: {
"Access-Control-Allow-Origin": "https://32blog.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
// OPTIONSリクエスト(preflight)にも応答する
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "https://32blog.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
または next.config.ts でまとめて設定する。
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "https://32blog.com" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST,OPTIONS" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type" },
],
},
];
},
};
export default nextConfig;
Access-Control-Allow-Origin: * はすべてのオリジンからのアクセスを許可する。パブリックなAPIなら問題ないが、認証が必要なAPIでは特定のオリジンのみ許可するよう制限すること。
まとめ
Vercelデプロイで踏むエラーのパターンをまとめる。
| エラー | 原因 | 解決策 |
|---|---|---|
| ビルド失敗 | 環境変数未設定、型エラー、ESLintエラー | Vercelに環境変数を追加。tsc --noEmit で事前確認 |
| Function Timeout | 処理が遅い、無限ループ | maxDuration 設定、非同期キュー、Edge Runtime |
| OOMエラー | メモリ不足 | NODE_OPTIONS=--max-old-space-size=4096、ストリーミング |
| 404(本番のみ) | 動的ルートの生成漏れ、ミドルウェアのmatcher設定ミス | dynamicParams、matcherを見直す |
| キャッシュが古い | fetch のキャッシュ設定不備 | revalidate または cache: "no-store" を明示 |
| CORSエラー | CORSヘッダー未設定 | APIルートまたは next.config.ts でヘッダーを追加 |
「ローカルで動く = Vercelで動く」ではない。デプロイ前に tsc --noEmit と npm run lint を走らせるのを習慣にするだけで、ビルド起因のエラーの半分は事前に潰せる。残りは環境変数の確認と、VercelのFunctionsログを丁寧に読むことで解決できる。