注意点・パフォーマンス
ランタイム依存の罠、ルータの選び方、cold start、エッジ特有の制約まとめ。
ルータの選択
Hono は3つのルータから選べる:
| ルータ | 速度 | 特徴 |
|---|---|---|
| RegExpRouter | 最速 | 正規表現に変換。スマートルーティング |
| TrieRouter | 速い | Trie 木構造。動的ルートに強い |
| SmartRouter | 状況による | RegExpRouter + TrieRouter のフォールバック(既定) |
| PatternRouter | 遅め | URLPattern API ベース |
| LinearRouter | 遅い | 動的に追加するルートが少ない時に |
import { Hono } from "hono/quick" // RegExpRouter(速い)
// import { Hono } from "hono" // SmartRouter(既定)
// import { Hono } from "hono/tiny" // PatternRouter(最小)
const app = new Hono()
cold start を抑える
- Hono コアは数 KB なのでcold start が短い。これが大きな利点
- 大量の依存(zod / dayjs / 巨大ライブラリ)を import するとcold start が遅くなる
- サーバレスでは必要なときだけ動的 importするのもアリ
app.post("/heavy", async (c) => {
const { lib } = await import("./heavy-lib") // 動的 import
return c.json(lib.process(...))
})
エッジ環境の制限
Cloudflare Workers / Vercel Edge / Deno Deploy 等のエッジでは:
- Node 専用 API は使えない(
fs/net/tls等) - npm パッケージが動かないことがある(
bcrypt/sharp等の native) - 実行時間制限(CF Workers: 30秒、CPU 時間 10ms / 30秒)
- メモリ制限(CF Workers: 128MB)
- ファイルシステムアクセス不可
代替: Web Crypto / WebAssembly / 外部サービス(R2, KV, D1)を使う。
ストリーミング
大きなレスポンスはストリーミングで。basics 参照。 メモリに全部載せず、生成しながら送る。
キャッシュ戦略
- 静的レスポンス:
Cache-Control: public, max-age=...+ CDN - 動的だが少し古くてOK:
stale-while-revalidate - Cloudflare Cache API:
cacheミドルウェア - KV / Durable Objects: 動的キャッシュの実体
並列リクエスト
外部 API を複数叩く時は Promise.all:
app.get("/dashboard", async (c) => {
const [user, posts, notifications] = await Promise.all([
fetch(...).then(r => r.json()),
fetch(...).then(r => r.json()),
fetch(...).then(r => r.json()),
])
return c.json({ user, posts, notifications })
})
レスポンス遅延を減らすテク
- JSON パースは必要なフィールドだけ使う
- 外部呼び出しは並列化(前述)
- レスポンスを返した後に重い処理は
executionCtx.waitUntil - 静的生成 / プリレンダリングできるものは事前に
- 圧縮は CDN/エッジ側に任せる(Hono の
compressも可)
エラーハンドリング
import { HTTPException } from "hono/http-exception"
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
// それ以外は 500
console.error(err)
return c.json({ error: "Internal Server Error" }, 500)
})
// ハンドラ内で
throw new HTTPException(404, { message: "Not Found", res: c.notFound() })
Sentry 等のエラー監視
import * as Sentry from "@sentry/cloudflare"
app.onError((err, c) => {
Sentry.captureException(err)
return c.json({ error: "Server error" }, 500)
})
ログ
- Cloudflare Workers:
console.log→wrangler tailで見える / Logpush で外部送信 - Bun / Node: stdout を Datadog / Better Stack 等へ
- 構造化ログ:
JSON.stringify({ level, msg, ...meta })
セキュリティ
- secure-headers ミドルウェアで一気にヘッダーを付ける
- cors はオリジンを明示。
*+ 認証は危険 - csrf は Cookie 認証で必須
- 入力は必ず検証(zod-validator)
- シークレットは必ず secret 管理(環境変数 / Bindings)
- SQL はプリペアドステートメント(D1, Drizzle ORM 等で)
- レート制限を入れる(自前 / Cloudflare Rate Limiting)
OpenAPI でクライアント生成
@hono/zod-openapi でOpenAPI スキーマを出して、
他言語クライアント(Swift / Kotlin / Python)を openapi-generator で生成できる。
よくある落とし穴
app.get(...)を変数代入で書くと型推論が崩れる — チェーンするawait c.req.json()を 2 回呼ぶ — 一度しか読めないres.json()後にres.text()等 — Body は1回限り- Cloudflare Workers で
fsを使う — 動かない - Promise を await し忘れ —
onErrorに届かない - 巨大な JSON をそのまま返す — メモリ枯渇。ストリーミング検討
パフォーマンスの計測
- Server-Timing:
timingミドルウェアで自動付与 - Cloudflare Workers Analytics: 実行時間 / エラー率 / リクエスト数
- Sentry Performance: トランザクション
- ベンチマーク:
autocannon(Node)/oha(Rust 製・速い)
テストのコツ
app.request()でランタイム不要テスト- 外部依存は mock(
vitestのvi.mock) - 環境変数は テスト用 fixture で渡す(
app.fetch(req, env)) - D1 は
better-sqlite3で代替テスト DB を作る方法あり
学び方
- 公式ドキュメントが秀逸
- example レポジトリを読む(github.com/honojs/examples)
- 1度作ったら複数ランタイムに移植してみると Hono の真価が分かる