Edge Runtime vs Node.js Runtime: サーバーレス関数が突然動かなくなる理由
Edge Runtime vs Node.js Runtime: サーバーレス関数が突然動かなくなる理由
みんな一度は聞いたことあると思います。Edge関数は速くて、安くて、ユーザーの近くで動く。コールドスタートも秒じゃなくてミリ秒単位。グローバル配信がデフォルト。なのでNext.jsのAPIルートにexport const runtime = 'edge'って1行足してデプロイしたら...
全部壊れた。
Edge Runtimeデバッグ地獄へようこそ。
エラーメッセージも暗号みたい。"Dynamic code evaluation not supported." "Module not found." "This API is not available." ローカルでは動いてたのに。Node.jsでも問題なかったのに。Edgeつけた途端これ。
Edge Runtimeの悪口じゃないです—本当にいい技術なんです。でも「Edge速い」マーケティングと、本番コード動かす現実の間にはかなりギャップがある。今日はそのギャップを埋めましょう。
まず概念から:Edge Runtimeって何?
デバッグの前に、何を扱ってるか理解しましょう。Edge RuntimeとNode.js Runtimeは、単にコードが動く場所が違うだけじゃないです。完全に別の実行環境。APIも違う、制約も違う、頭の中のモデルも変えなきゃいけない。
Node.js Runtime:フル装備
Node.jsランタイムはみんな知ってるやつ。サーバーで動くフルスペックのNode.js環境:
// Node.jsランタイムならこれ全部動く import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { spawn } from 'child_process'; export async function POST(request) { // ファイル読み込み?できる const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); // ネイティブcrypto?できる const hash = crypto.createHash('sha256').update('secret').digest('hex'); // プロセス起動?できる const child = spawn('ls', ['-la']); // npmパッケージ何でも?できる const pdf = await generatePDF(data); return Response.json({ success: true }); }
Node.jsランタイムで得られるもの:
- Node.jsコアモジュール全部(
fs、path、crypto、child_processなど) - ネイティブモジュール(C++アドオン、NAPIでRustバインディング)
- 実質コードサイズ制限なし
- 長いタイムアウト(大体5分まで)
- ネイティブドライバーでDB接続
- スタックトレースちゃんと出るデバッグ
代わりに?コールドスタート250ms〜1秒+、リージョン別デプロイでグローバルじゃない、スケールするとコストも増える。
Edge Runtime:軽くて速い、でも...
Edge Runtimeは完全に別物。Web APIとV8 isolatesベース—Cloudflare Workers動かしてるのと同じ技術:
// Edge Runtimeではこれだけ export const runtime = 'edge'; export async function POST(request) { // Web Fetch API?できる const response = await fetch('https://api.example.com/data'); // Web Crypto API?できる(でもNode cryptoと違う!) const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('secret') ); // Headers, Request, Response?できる const headers = new Headers(); headers.set('Cache-Control', 'max-age=3600'); return new Response(JSON.stringify({ success: true }), { headers }); }
Edge Runtimeで得られるもの:
- Web Platform APIだけ(Fetch、Streams、Crypto、URLなど)
- 10ms切るコールドスタート
- グローバル配信(300箇所+で実行)
- スケールしてもコスト低い
- 1〜4MBコードサイズ制限
- 短いタイムアウト(大体30秒まで)
代わりに?fsなし、ネイティブモジュールなし、Node.jsコアモジュールなし、npmパッケージ半分動かない。
なぜ壊れるか:失敗パターン別
壊れる理由をパターン別に整理しましょう。何が問題か分かれば直せる。
パターン1:Node.jsコアモジュールがない
一番多い。Edgeにないものをimportしてる:
// ❌ Edge全部ダメ import fs from 'fs'; // ファイルシステムなし import path from 'path'; // pathモジュールなし import crypto from 'crypto'; // crypto APIが違う import { Buffer } from 'buffer'; // Buffer一部だけ import stream from 'stream'; // Node streamsなし import http from 'http'; // httpモジュールなし import https from 'https'; // httpsモジュールなし import net from 'net'; // TCPソケットなし import dns from 'dns'; // DNS lookupなし import child_process from 'child_process'; // プロセスなし import os from 'os'; // OS情報なし import worker_threads from 'worker_threads'; // ワーカースレッドなし
解決: Web API代替かpolyfill使う:
// ✅ Edgeで使える代替 // crypto.createHash()の代わり async function sha256(message) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Buffer.from()の代わり function base64Encode(str) { return btoa(String.fromCharCode(...new TextEncoder().encode(str))); } function base64Decode(base64) { return new TextDecoder().decode( Uint8Array.from(atob(base64), c => c.charCodeAt(0)) ); } // path.join()の代わり function joinPath(...segments) { return segments.join('/').replace(/\/+/g, '/'); } // querystringの代わりにURL使う function parseQuery(url) { return Object.fromEntries(new URL(url).searchParams); }
パターン2:動的コード実行禁止
Edge Runtimeはセキュリティ上eval()とnew Function()使えません。思ってるより多くのパッケージがこれで壊れる:
// ❌ Edge全部ダメ eval('console.log("hello")'); new Function('return 1 + 1')(); require('vm').runInNewContext('1 + 1'); // vmモジュールもなし // こういうパッケージが内部で使ってる: // - 一部テンプレートエンジン(Handlebars、特定モードのEJS) // - 一部スキーマ検証ライブラリ // - 一部シリアライズライブラリ // - ソースマップ処理
エラーメッセージ:
Dynamic code evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile')
not allowed in Edge Runtime
解決: 代替パッケージ探す:
// lodash templateは使えない(内部でnew Function使ってる) // ❌ import template from 'lodash/template'; const compiled = template('Hello <%= name %>'); // ✅ 静的テンプレートライブラリに変える import Mustache from 'mustache'; const output = Mustache.render('Hello {{name}}', { name: 'World' }); // または普通にテンプレートリテラル function html(strings, ...values) { return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '' ); } const name = 'World'; const output = html`Hello ${name}`;
パターン3:ネイティブモジュール
C++/Rustバインディング使ってるパッケージはEdgeで動かない:
// ❌ Edgeで動かないパッケージ import sharp from 'sharp'; // 画像処理(ネイティブ) import bcrypt from 'bcrypt'; // パスワードハッシュ(ネイティブ) import canvas from 'canvas'; // キャンバス(ネイティブ) import sqlite3 from 'sqlite3'; // SQLite(ネイティブ) import puppeteer from 'puppeteer'; // ブラウザ自動化(ネイティブ) import prisma from '@prisma/client'; // Prisma(クエリエンジンがネイティブ)
エラーメッセージ:
Error: Cannot find module 'sharp'
Module build failed: Native modules are not supported in Edge Runtime
解決: Edge対応の代替使う:
// ✅ Edge対応代替 // bcryptの代わり(純粋JS実装) import { hash, compare } from 'bcryptjs'; // sharpの代わり // クラウド画像サービスかWASMベースのソリューション async function resizeImage(imageUrl, width, height) { const response = await fetch( `https://images.example.com/resize?url=${encodeURIComponent(imageUrl)}&w=${width}&h=${height}` ); return response; } // Prismaの代わり // Edge対応ドライバアダプタ使う import { PrismaClient } from '@prisma/client'; import { PrismaNeon } from '@prisma/adapter-neon'; // またはサーバーレス向けORM import { drizzle } from 'drizzle-orm/neon-http'; // SQLiteの代わり // D1(Cloudflare)、Turso、PlanetScale使う
パターン4:ブロッキング処理
Edge Runtimeは速いノンブロッキング処理用。イベントループ止めるのは問題:
// ❌ Edgeで問題になるパターン import { readFileSync } from 'fs'; // 同期ファイル読み込み(+fsもない) sleep(1000); // ブロッキングsleep // 重い同期計算 function fibonacciSync(n) { if (n <= 1) return n; return fibonacciSync(n - 1) + fibonacciSync(n - 2); } const result = fibonacciSync(45); // 数秒止まる // 巨大JSON同期パース const hugeData = JSON.parse(hugeJsonString); // タイムアウトの可能性
解決: ストリーミングとチャンク処理:
// ✅ Edgeフレンドリーなパターン // 大きいレスポンスはストリーミング export async function GET() { const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 1000; i++) { const chunk = await fetchChunk(i); controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n')); // 他の処理に譲る await new Promise(resolve => setTimeout(resolve, 0)); } controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } }); } // Web Streamsでパース async function* parseJsonStream(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.trim()) { yield JSON.parse(line); } } } }
パターン5:サイズ超過
Edge関数はサイズ制限きつい。Vercel: 1〜4MB、Cloudflare Workers: 1〜10MB:
// ❌ バンドルサイズ爆発 import _ from 'lodash'; // 70KB+ minified import moment from 'moment'; // 300KB+ locale込み import * as AWS from 'aws-sdk'; // デカすぎ import 'core-js/stable'; // 150KB+ polyfill // アイコンライブラリ丸ごと import * as Icons from '@heroicons/react/24/solid'; // バンドルにMLモデルとか大きいデータ import model from './large-ml-model.json'; // 2MBモデル
解決: tree shakeしっかり + lazy load:
// ✅ サイズ最適化 // lodash全体じゃなくて import groupBy from 'lodash/groupBy'; import debounce from 'lodash/debounce'; // momentじゃなくて import { format, parseISO } from 'date-fns'; // aws-sdk v2じゃなくて import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; // アイコン個別import import { HomeIcon } from '@heroicons/react/24/solid'; // 大きいデータはランタイムでfetch export async function GET() { const model = await fetch('https://cdn.example.com/model.json').then(r => r.json()); // ... }
実践デバッグケース
よくあるEdge Runtime失敗ケース見ていきましょう。
ケース1:突然の "Module Not Found"
状況: ローカルで動く、ビルド通る、デプロイするとランタイムで壊れる。
Error: Cannot find module 'util'
at EdgeRuntime (edge-runtime.js:1:1)
原因特定:
// 1. 何が'util'使ってるか探す // 依存関係掘る必要あり // package.jsonは問題なさそう { "dependencies": { "next": "14.0.0", "jsonwebtoken": "9.0.0", // <-- 犯人! "next-auth": "4.24.0" } } // 2. 依存関係ツリー確認 // npm ls util 打つと // 出力: // └─┬ [email protected] // └── [email protected] // jsonwebtokenが内部でNode util使ってた!
解決:
// 方法1:Edge対応JWTライブラリに変える import { SignJWT, jwtVerify } from 'jose'; // Edge対応! export const runtime = 'edge'; export async function POST(request) { const secret = new TextEncoder().encode(process.env.JWT_SECRET); // 署名 const token = await new SignJWT({ userId: '123' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(secret); // 検証 const { payload } = await jwtVerify(token, secret); return Response.json({ token, payload }); } // 方法2:このルートはEdge使わない // export const runtime = 'edge'; 消す // Node.jsランタイムで動かす
ケース2:NextAuth.jsセッション爆発
状況: ローカルでログインできる、Edgeプロダクションで壊れる。
Error: next-auth requires a secret to be set in production
または
Error: [next-auth]: `useSession` must be wrapped in a <SessionProvider />
// (ラップしてるのに?昨日まで動いてたのに?)
原因:
// next-authのルートハンドラがEdge完全対応じゃない // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; export const runtime = 'edge'; // ❌ これが問題! export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub], // DBアダプタもEdgeで動かない adapter: PrismaAdapter(prisma), // ❌ ネイティブ依存! });
解決:
// app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; // 方法1:認証はNode.jsで(今のところ推奨) export const runtime = 'nodejs'; // ✅ 明示的Node.js // 方法2:Edge対応アダプタ使う import { DrizzleAdapter } from '@auth/drizzle-adapter'; import { drizzle } from 'drizzle-orm/neon-http'; export const { handlers, auth } = NextAuth({ providers: [GitHub], adapter: DrizzleAdapter(drizzle(process.env.DATABASE_URL)), // ✅ Edge対応 }); // ミドルウェアはEdge使える(読み取りだけ、書き込みNG) // middleware.js export { auth as middleware } from './auth'; export const config = { matcher: ['/dashboard/:path*'] };
ケース3:DB接続地獄
状況: ローカルでクエリ動く、Edgeプロダクションで壊れる。
Error: Connection pool exhausted
Error: prepared statement "s0" already exists
Error: Cannot establish database connection
原因:
// 普通のDB接続はEdgeで動かない // Edge関数は状態なくて世界中に散らばってる import { Pool } from 'pg'; // ❌ コネクションプールEdgeで動かない const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Edgeでは意味ない }); // Edge呼び出しごとに独立 - 共有コネクションプールなし! // 300箇所+から同時に1つのDBに接続 = 破綻
解決:
// ✅ Edge対応DBパターン // 方法1:HTTPベースDB接続(Edge最適) import { neon } from '@neondatabase/serverless'; export const runtime = 'edge'; export async function GET() { const sql = neon(process.env.DATABASE_URL); // クエリごとに別HTTPリクエスト - 接続管理不要 const users = await sql`SELECT * FROM users LIMIT 10`; return Response.json({ users }); } // 方法2:Prisma Data Proxy import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.PRISMA_DATA_PROXY_URL, // 直接接続じゃなくてdata proxy経由 }, }, }); // 方法3:重いDB処理はEdge使わない export const runtime = 'nodejs'; // コールドスタート許容 import { db } from '@/lib/database'; export async function GET() { const users = await db.user.findMany(); return Response.json({ users }); }
判断基準:いつどっち使う?
デバッグで苦しんだ後気になるよね。Edgeっていつ使うの?
Edge使う時:
1. レイテンシ大事 + キャッシュできるデータ:
// ✅ Edge最適:地域ベースルーティング、キャッシュコンテンツ export const runtime = 'edge'; export async function GET(request) { const country = request.geo?.country || 'US'; const content = await fetch(`https://cdn.example.com/content/${country}.json`, { next: { revalidate: 3600 } }); return Response.json(await content.json()); }
2. 認証チェック(読み取りだけ):
// ✅ Edge最適:JWT検証、権限確認 export const runtime = 'edge'; export async function middleware(request) { const token = request.cookies.get('session'); if (!token) { return Response.redirect(new URL('/login', request.url)); } try { await jwtVerify(token.value, secret); return NextResponse.next(); } catch { return Response.redirect(new URL('/login', request.url)); } }
3. A/Bテスト、フィーチャーフラグ:
// ✅ Edge最適:即判断、バックエンド呼び出し不要 export const runtime = 'edge'; export async function middleware(request) { const bucket = Math.random(); const variant = bucket < 0.5 ? 'control' : 'treatment'; const response = NextResponse.next(); response.cookies.set('experiment_variant', variant); return response; }
4. シンプルなリダイレクト、ヘッダー操作:
// ✅ Edge最適:URLリライト、セキュリティヘッダー export const runtime = 'edge'; export async function middleware(request) { const url = request.nextUrl.clone(); if (url.pathname.startsWith('/old-blog/')) { url.pathname = url.pathname.replace('/old-blog/', '/blog/'); return Response.redirect(url); } const response = NextResponse.next(); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); return response; }
Node.js使う時:
1. コネクションプール必要なDB処理:
// ✅ Node.js:従来のDB、接続維持必要 export const runtime = 'nodejs'; import { prisma } from '@/lib/prisma'; export async function GET() { const users = await prisma.user.findMany({ include: { posts: true }, take: 50, }); return Response.json({ users }); }
2. ファイル処理、バイナリ:
// ✅ Node.js:ファイルシステム、ネイティブバイナリ export const runtime = 'nodejs'; import sharp from 'sharp'; import { writeFile } from 'fs/promises'; export async function POST(request) { const formData = await request.formData(); const file = formData.get('image'); const buffer = Buffer.from(await file.arrayBuffer()); const processed = await sharp(buffer) .resize(800, 600) .webp({ quality: 80 }) .toBuffer(); await writeFile(`./uploads/${file.name}.webp`, processed); return Response.json({ success: true }); }
3. 複雑なビジネスロジック:
// ✅ Node.js:npmエコシステムフルに使う時 export const runtime = 'nodejs'; import Stripe from 'stripe'; import { sendEmail } from '@/lib/email'; import { generatePDF } from '@/lib/pdf'; import { prisma } from '@/lib/prisma'; export async function POST(request) { const order = await request.json(); const stripe = new Stripe(process.env.STRIPE_SECRET); const payment = await stripe.paymentIntents.create({...}); await prisma.order.update({...}); const pdf = await generatePDF(order); await sendEmail({ to: order.email, subject: '注文確認', attachments: [{ filename: 'invoice.pdf', content: pdf }], }); return Response.json({ success: true }); }
4. 時間かかる処理:
// ✅ Node.js:30秒超える時 export const runtime = 'nodejs'; export const maxDuration = 300; // 5分 export async function POST(request) { const { videoUrl } = await request.json(); const processed = await processVideo(videoUrl); // 2〜3分かかる return Response.json({ downloadUrl: processed.url, duration: processed.duration }); }
ハイブリッド:両方使う
正解は「EdgeかNode.jsか」じゃなくて、両方戦略的に使うこと:
src/
├── app/
│ ├── api/
│ │ ├── auth/ # Node.js - DBセッション
│ │ ├── payments/ # Node.js - Stripe、複雑なロジック
│ │ ├── upload/ # Node.js - ファイル処理
│ │ ├── geo/ # Edge - 位置ベース
│ │ ├── flags/ # Edge - フィーチャーフラグ
│ │ └── health/ # Edge - ヘルスチェック
│ └── middleware.ts # Edge - 認証、リダイレクト
// middleware.ts - 全体でEdge import { auth } from './auth'; export default auth((request) => { if (!request.auth && request.nextUrl.pathname.startsWith('/dashboard')) { return Response.redirect(new URL('/login', request.url)); } }); export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
// app/api/geo/route.ts - 速い地域検索はEdge export const runtime = 'edge'; export async function GET(request) { const { country, city, latitude, longitude } = request.geo || {}; const nearestStore = await fetch( `https://api.stores.com/nearest?lat=${latitude}&lng=${longitude}`, { next: { revalidate: 3600 } } ).then(r => r.json()); return Response.json({ nearestStore, userLocation: { country, city } }); }
// app/api/orders/route.ts - 複雑な処理はNode.js export const runtime = 'nodejs'; export async function POST(request) { // 注文処理フルパワー }
まとめ
Edge Runtimeはいい技術だけど万能じゃない。Node.jsフル機能をグローバル配信と10msコールドスタートに交換した制約ある環境。この制約—モジュールなし、動的評価禁止、ネイティブNG、サイズ制限、状態なし—を分かってればEdgeうまく使える。
ポイント:
-
Edgeは速くてシンプルな処理用 — 認証チェック、リダイレクト、A/Bテスト、キャッシュコンテンツ。複雑なビジネスロジックじゃない。
-
迷ったらNode.js — コールドスタート250ms〜1秒許容して、API完全互換確保。
-
DB戦略重要 — Edgeは HTTP接続(Neon、PlanetScale HTTP、Prisma Data Proxy)、Node.jsはコネクションプール。
-
依存関係掘る —
npm lsでNodeコアモジュール使ってるパッケージ探す。Edge対応に変える。 -
サイズ気にする — tree shakeしっかり、データはlazy load、barrel fileじゃなくて具体的import。
-
ハイブリッドが正解 — ミドルウェアとレイテンシ重要なルートはEdge、残りはNode.js。
次export const runtime = 'edge'つけて関数壊れたら、どこ見ればいいかもう分かるはず。Edge Runtimeが壊れてるんじゃなくて、違うだけ。その違い分かれば罠踏まずにうまく使える。