2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発でVercelのコールドスタート問題に悩んで、最終的にEdge Runtimeに移行した話

2
Posted at

個人開発でVercelのコールドスタート問題に悩んで、最終的にEdge Runtimeに移行した話

はじめに

個人開発で宅建試験対策のAIアプリ「takkenai.jp」を作っています。1250問以上の問題をAIが解説してくれるWebアプリで、Next.js + Vercelという王道構成で運用しています。

ある日、ユーザーから「問題を解き始めるときに、最初の1問だけやたら表示が遅い」という報告をもらいました。

自分でも試してみると——確かに遅い。体感で3〜5秒くらい待たされる。2問目以降はサクサクなのに、最初の1問だけがもたつく。

これが、Vercelのコールドスタート問題との戦いの始まりでした。

問題発覚:「最初の1問だけ遅い」の正体

再現と計測

まずはちゃんと数字で把握しようと思い、Vercelのダッシュボードとconsole.timeで計測しました。

// app/api/questions/route.ts(当時のコード)
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  console.time('question-fetch');

  const questions = await fetchQuestionsFromDB();
  const aiExplanation = await generateExplanation(questions[0]);

  console.timeEnd('question-fetch');
  return NextResponse.json({ questions, aiExplanation });
}

計測結果がこちらです。

状態 レイテンシ(p50) レイテンシ(p95)
ウォームスタート 180ms 320ms
コールドスタート 2,800ms 4,500ms

p95で4.5秒。個人開発のアプリで、問題を表示するだけに4秒半はさすがにきつい。

コールドスタートの発生頻度

Vercel Functionsのログを1週間分集計してみると、こんな感じでした。

  • 1日あたりのAPIコール数: 約800〜1,200回
  • コールドスタート発生率: 約18〜22%
  • 特に多い時間帯: 早朝5〜7時、深夜1〜3時(アクセスが途切れる時間帯の直後)

個人開発のアプリなので、当然トラフィックにムラがあります。勉強アプリという性質上、通勤時間帯にアクセスが集中し、それ以外の時間は閑散としている。結果として、約5回に1回はコールドスタートに当たるという状況でした。

原因調査:なぜこんなに遅いのか

Vercel Serverless Functions(Lambda)の仕組み

Vercelのデフォルトのサーバレス関数は、AWSのLambdaベースで動いています。リクエストが来るたびにコンテナが起動し、一定時間アイドル状態が続くとコンテナが破棄される仕組みです。

コールドスタート時に何が起きているかを分解すると:

  1. コンテナの起動: 約500〜800ms
  2. Node.jsランタイムの初期化: 約300〜500ms
  3. モジュールの読み込み(import): 約400〜800ms
  4. DB接続の確立: 約200〜400ms
  5. 実際のビジネスロジック: 約150〜300ms

ビジネスロジック自体は大したことないのに、そこに到達するまでに2秒近くかかっているわけです。

最初にやった小手先の対策(効果薄)

Edge Runtimeに移行する前に、いくつか試しました。

① dynamic importの削減

// Before: dynamic import
const { PrismaClient } = await import('@prisma/client');

// After: top-level import
import { PrismaClient } from '@prisma/client';

② グローバル変数でDB接続を使い回し

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

③ バンドルサイズの削減

不要な依存を削り、Route Handlerのバンドルサイズを1.2MBから680KBまで減らしました。

結果は——コールドスタートが4,500msから3,200msに改善

悪くはないけど、まだ3秒。ユーザー体験として全然ダメです。

試行錯誤:Edge Runtimeへの移行

Edge Runtimeとは何が違うのか

VercelのEdge Runtimeは、Lambda(Node.js)ではなく、V8 Isolateベースの軽量ランタイム上で動作します。Cloudflare Workersと似た仕組みで、コンテナ起動のオーバーヘッドが極めて小さいのが特徴です。

比較項目 Serverless (Lambda) Edge Runtime
ランタイム Node.js V8 Isolate
コールドスタート 1〜5秒 0〜50ms
最大実行時間(Hobby) 10秒 25秒
使えるAPI Node.js全般 Web Standard APIs
リージョン 単一 エッジ(グローバル)

コールドスタートが0〜50ms。桁が2つ違う。これは試す価値がある。

移行でぶつかった壁

ただし、Edge Runtimeには制約があります。ここでかなりやらかしました。

壁①:Prismaがそのまま動かない

Edge RuntimeはNode.jsのネイティブAPIが使えないため、通常のPrisma Clientは動きません。

最初は何も考えずに export const runtime = 'edge' を追加して、デプロイしたら500エラーの嵐でした。

Error: PrismaClient is unable to run in an edge runtime.

解決策として、Prisma Accelerate(Prismaのエッジ対応プロキシ)を導入しました。

// lib/db.ts
import { PrismaClient } from '@prisma/client/edge';
import { withAccelerate } from '@prisma/extension-accelerate';

export const prisma = new PrismaClient({
  datasourceUrl: process.env.DATABASE_URL,
}).$extends(withAccelerate());

壁②:一部のnpmパッケージが動かない

cryptoモジュールに依存しているパッケージがいくつか使えませんでした。認証周りで使っていた処理を、Web Crypto APIベースに書き換える必要がありました。

// Before: Node.js crypto
import crypto from 'crypto';
const hash = crypto.createHash('sha256').update(data).digest('hex');

// After: Web Crypto API
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

壁③:レスポンスボディサイズの制限

Edge Runtimeでは、レスポンスボディが4MBに制限されています。問題データを大量に返すAPIエンドポイントが1つあり、これに引っかかりました。ページネーションを導入して対処しました。

最終的なRoute Handlerの構成

// app/api/questions/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';

// この1行を追加するだけ…と言いたいが、裏側の対応が色々必要だった
export const runtime = 'edge';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category');
  const page = parseInt(searchParams.get('page') ?? '1', 10);
  const limit = 20;

  try {
    const questions = await prisma.question.findMany({
      where: category ? { category } : undefined,
      skip: (page - 1) * limit,
      take: limit,
      cacheStrategy: { ttl: 300 }, // Prisma Accelerateのキャッシュ: 5分
    });

    return NextResponse.json({
      questions,
      page,
      hasMore: questions.length === limit,
    });
  } catch (error) {
    console.error('Failed to fetch questions:', error);
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    );
  }
}

結果:数字で見る改善効果

移行後、2週間分のデータを集計しました。

指標 Before (Lambda) After (Edge) 改善率
コールドスタート p50 2,800ms 22ms 99.2%減
コールドスタート p95 4,500ms 48ms 98.9%減
ウォームスタート p50 180ms 95ms 47%減
コールドスタート発生率 18〜22% 実質0%(体感不可)

コールドスタートのレイテンシが2,800ms → 22ms。もはやコールドスタートという概念が消えたに等しい。

ウォームスタート時も180ms → 95msに改善しています。これはEdge Runtimeがユーザーに近いエッジロケーションで実行されることと、Prisma Accelerateのキャッシュが効いているためです。

ユーザーからの「最初だけ遅い」という報告も、移行後はゼロになりました。

移行してわかったこと・注意点

Edge Runtimeが向いているケース

  • 読み取り中心のAPI(今回のような問題データの取得)
  • レスポンスが軽量(JSON数十KB程度)
  • Node.js固有のAPIに依存していない処理

向いていないケース

  • 重い計算処理(CPU時間の制限がある)
  • 大きなnpmパッケージへの依存(バンドルサイズ制限)
  • ファイルシステム操作fsモジュールは使えない)

実際、takkenai.jpでもAI解説生成の一部エンドポイントはLambdaのまま残しています。外部AI APIの呼び出しでストリーミングレスポンスを返す部分は、Edge Runtimeでも動くのですが、タイムアウトやエラーハンドリングの都合でNode.jsランタイムの方が扱いやすかったためです。

全部をEdgeにする必要はなく、エンドポイントごとに使い分けるのがベストだと感じました。

まとめ

個人開発でトラフィックが安定しないアプリにおいて、Vercelのコールドスタートは地味に致命的です。特にユーザーが「さあ勉強するぞ」と思って開いた最初の1問で3〜5秒待たされるのは、離脱に直結します。

Edge Runtimeへの移行は、Prisma周りの対応など多少の手間はかかりましたが、コールドスタートのレイテンシが99%減という劇的な改善が得られました。

同じ問題で悩んでいる個人開発者の方の参考になれば幸いです。


今回の話の舞台になった takkenai.jp は、AIが宅建試験の過去問1250問以上を丁寧に解説してくれるWebアプリです。2025年の宅建試験を目指している方は、ぜひ使ってみてください。コールドスタートはもう気にならないはずです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?