📖

Next.js と Nodemailer による問い合わせフォームの送信高速化

に公開

はじめに

Next.js の問い合わせフォームでメールを送信すると、完了まで 約 3 秒 かかっていました。
今回は SMTP コネクションプール非同期レスポンス返却 を取り入れ、1 秒未満 に短縮した流れをまとめます。

前提

  • Next.js (App Router)
  • Nodemailer + Gmail SMTP
  • Vercel Functions でホスティング

変更ポイントの全体像

課題 対応
送信のたびに SMTP 接続を再生成していた transporter をモジュール外に出し、pool: true でコネクションを使い回します
await sendMail() で処理をブロックしていた await を外し、バックグラウンドで送信します
応答までの待機時間が長い API は 202 Accepted を即返却して、ユーザーを待たせません

コード比較

変更前

// src/app/api/contact/route.ts
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";

// .env.local で設定した環境変数を使います
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: Number(process.env.SMTP_PORT || 587),
  secure: Number(process.env.SMTP_PORT) === 465,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

export async function POST(request: Request) {
  try {
    const { name, email, message } = await request.json();

    if (!name || !email || !message) {
      return NextResponse.json({ error: "名前・メール・メッセージは必須です。" }, { status: 400 });
    }

    const mailOptions = { /* 省略 */ };

    // メール送信
    await transporter.sendMail(mailOptions);

    return NextResponse.json({ ok: true });
  } catch (err) {
    console.error("メール送信エラー:", err);
    return NextResponse.json({ error: "送信中にエラーが発生しました。" }, { status: 500 });
  }
}
  • 送信が完了するまで await で待っているため、レスポンスが遅くなります。

変更後

// src/app/api/contact/route.ts
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";

// transporter をファイル外側に定義し、プールを有効化
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: Number(process.env.SMTP_PORT),
  secure: process.env.SMTP_PORT === "465",
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  pool: true,
  maxConnections: 5,
  maxMessages: Infinity,
});

export async function POST(request: Request) {
  const { name, email, message } = await request.json();

  if (!name || !email || !message) {
    return NextResponse.json({ error: "名前・メール・メッセージは必須です。" }, { status: 400 });
  }

  const mailOptions = { /* 省略 */ };

  // await せずバックグラウンド送信
  transporter
    .sendMail(mailOptions)
    .then(() => console.log("メール送信完了"))
    .catch((err) => console.error("メール送信失敗", err));

  // 即時応答
  return NextResponse.json({ queued: true }, { status: 202 });
}
  • コネクションプールにより TLS ハンドシェイクコストを削減しています。
  • sendMail は非同期で実行し、API はすぐに 202 を返します。

手順詳細

1. transporter を共通化する

ファイルの上部で Nodemailer の createTransport を呼び出し、pool: true を指定します。
こうすることで、コネクションを再利用でき、毎回のハンドシェイク時間を省けます。

2. sendMailawait を外す

メール送信が完了するのを API が待たないようにします。
アプリケーションから見ると「処理をキューに入れただけ」なので速く応答できます。

3. HTTP ステータスを 202 Accepted に変更

キューに受け付けたことを示すため 202 を返却しています。
フロントエンドではこのタイミングで「送信が完了しました」と表示して構いません。


効果測定

指標 変更前 変更後
/api/contact レスポンス時間 約 3000 ms 約 400 ms
体感 待ち時間を感じる すぐに完了表示

今後の検討ポイント

  • Gmail SMTP ではなく Gmail API (OAuth2) を使用し、HTTP ベースの呼び出しに切り替える
  • SendGrid や Amazon SES などトランザクショナルメール向けのサービスを利用する
  • ワーカーやタスクキューを導入し、完全に非同期で処理する

まとめ

コネクションプールと非同期レスポンスを組み合わせるだけで、問い合わせフォームの応答速度を大きく改善できました。

Discussion