c.json() 的性能陷阱:大对象序列化如何阻塞事件循环?
流式 JSON 响应的替代方案
在 Hono 中,c.json(data) 是一个简洁的工具,用于将 JavaScript 对象序列化为 JSON 并返回响应。然而,当处理大型数据对象时,c.json() 可能引发严重的性能问题,甚至阻塞事件循环,导致服务不可用。
一、问题本质:JSON.stringify() 是同步且 CPU 密集的操作
当你调用:
ts
c.json(largeData) // largeData 可能是 10MB+ 的数组或对象Hono 内部会执行:
ts
const body = JSON.stringify(data) // ← 同步、阻塞主线程
return new Response(body, { headers: { 'Content-Type': 'application/json' } })问题分析:
| 问题 | 说明 |
|---|---|
| 同步执行 | JSON.stringify() 在 V8 引擎中是同步操作,期间事件循环被完全阻塞 |
| CPU 密集 | 序列化大对象消耗大量 CPU 时间,可能导致高延迟 |
| 内存峰值 | 整个 JSON 字符串需在内存中构建,可能触发 GC 或 OOM |
| 不可扩展 | 在 Serverless 环境(如 Cloudflare Workers)中,CPU 时间受限,可能超时 |
实验:阻塞事件循环
ts
app.get('/big-json', (c) => {
const largeArray = Array(100_000).fill({ id: 1, name: 'Test', data: 'x'.repeat(100) })
return c.json(largeArray) // 可能阻塞数百毫秒
})- 同时发起多个请求 → 服务器响应变慢或超时;
- 其他请求被“饿死”,即使它们是轻量的。
二、解决方案:避免一次性序列化,改用流式响应
目标:将大数据分块生成和传输,避免长时间阻塞。
✅ 方案 1:使用 ReadableStream + 分块生成
ts
app.get('/stream-json', (c) => {
const stream = new ReadableStream({
async start(controller) {
// 模拟大数据源(如数据库游标、文件流)
const data = Array(50_000).fill(null).map((_, i) => ({ id: i, name: `User-${i}` }))
// 分块写入
const chunkSize = 1000
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize)
const jsonChunk = JSON.stringify(chunk)
// 发送分块
controller.enqueue(jsonChunk)
// 可选:微任务让步,避免长时间占用事件循环
if (i % 5000 === 0) {
await new Promise(resolve => setTimeout(resolve, 0))
}
}
controller.close()
}
})
const response = new Response(stream, {
headers: {
'Content-Type': 'application/json',
'X-Content-Type': 'streamed-json'
}
})
return response
})注意:此例返回的是多个 JSON 数组拼接,客户端需处理。更规范的做法见方案 3。
✅ 方案 2:NDJSON(Newline Delimited JSON)
更高效、标准的流式 JSON 格式:每行一个 JSON 对象。
ts
app.get('/ndjson', (c) => {
const stream = new ReadableStream({
async start(controller) {
const total = 50_000
// NDJSON 通常以 [ 开头
controller.enqueue('[\n')
for (let i = 0; i < total; i++) {
const obj = { id: i, name: `User-${i}` }
const line = JSON.stringify(obj) + (i < total - 1 ? ',\n' : '\n')
controller.enqueue(line)
// 每 1000 条让步一次
if (i % 1000 === 0) {
await new Promise(resolve => setTimeout(resolve, 0))
}
}
controller.enqueue(']')
controller.close()
}
})
return new Response(stream, {
headers: { 'Content-Type': 'application/x-ndjson' }
})
})客户端可逐行解析:
js
const response = await fetch('/ndjson')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() // 保留未完整行
for (const line of lines) {
if (line.trim()) {
const obj = JSON.parse(line.replace(/,$/, ''))
console.log(obj)
}
}
if (done) break
}✅ 方案 3:使用数据库游标 + 真正的流
适用于从数据库流式读取:
ts
app.get('/db-stream', async (c) => {
const db = c.env.DB // D1 or similar
const stream = new ReadableStream({
async start(controller) {
controller.enqueue('[\n')
// 假设支持游标或分页查询
let offset = 0
const limit = 1000
let hasMore = true
while (hasMore) {
const rows = await db.prepare(`
SELECT id, name FROM users LIMIT ? OFFSET ?
`).bind(limit, offset).all()
if (rows.results.length === 0) {
hasMore = false
} else {
// 分批序列化并发送
const chunk = rows.results.map(r => JSON.stringify(r)).join(',\n') + ',\n'
controller.enqueue(chunk)
offset += limit
// 让步
await new Promise(resolve => setTimeout(resolve, 0))
}
}
// 发送结尾
const finalChunk = buffer ? buffer + '\n]' : '\n]'
controller.enqueue(finalChunk.trimEnd().replace(/,\n\]$/, '\n]'))
controller.close()
}
})
return new Response(stream, {
headers: { 'Content-Type': 'application/x-ndjson' }
})
})三、性能对比
| 方案 | 阻塞事件循环 | 内存使用 | 客户端体验 | 适用场景 |
|---|---|---|---|---|
c.json(bigData) | ❌ 高 | ❌ 高 | ❌ 延迟高 | 小数据(< 100KB) |
ReadableStream + 分块 | ✅ 低 | ✅ 低 | ✅ 渐进加载 | 大列表、日志 |
| NDJSON 流 | ✅ 最佳 | ✅ 最佳 | ✅ 实时处理 | 数据导出、事件流 |
四、最佳实践建议
- 避免
c.json()处理 > 1MB 的数据; - 优先使用分页:
/api/users?page=1&limit=100比返回全部数据更合理; - 对大数据使用流式响应:
ReadableStream+NDJSON; - 控制流速:在
controller.enqueue()后适当让步(setTimeout(0)); - 客户端适配:使用
fetch+ReadableStream逐块处理; - 监控序列化时间:记录
JSON.stringify()耗时,设置告警。
五、结论
c.json() 的便利性背后隐藏着同步序列化的性能陷阱。在处理大型数据集时,应主动规避这一风险,采用流式 JSON 响应(如 NDJSON)或分页查询。这不仅能提升服务性能,还能改善用户体验,实现真正的“响应式” API。