Skip to content

c.html()c.xml()c.file():如何避免内存溢出?

支持 ReadableStream,实现大文件分块传输

在 Hono 中,c.html()c.xml()c.file() 是用于返回 HTML、XML 和文件响应的便捷方法。然而,当处理大文件或大型内容时,如果直接将整个内容读入内存再返回,极易导致内存溢出(OOM),尤其在 Serverless 环境(如 Cloudflare Workers)中,内存限制严格(通常 128MB~1GB),风险极高。


一、问题本质:一次性加载大内容到内存

❌ 危险做法:同步读取大文件
ts
// Node.js 环境示例(危险!)
import fs from 'fs'

app.get('/big.html', (c) => {
  const html = fs.readFileSync('./large-report.html', 'utf-8') // 整个文件进内存
  return c.html(html) // 内存占用翻倍(字符串 + Response body)
})
  • 一个 500MB 的 HTML 文件 → 至少 1GB 内存峰值;
  • 多个并发请求 → 服务崩溃。
c.file() 的陷阱
ts
app.get('/download', async (c) => {
  const file = await c.env.MY_BUCKET.get('huge-video.mp4')
  if (!file) return c.notFound()

  // file.arrayBuffer() 会将整个文件加载到内存
  const buffer = await file.arrayBuffer()
  return c.file(buffer, { filename: 'video.mp4' }) // ❌ 内存溢出风险
})

arrayBuffer() 将整个文件读入内存,不适合大文件。


二、解决方案:使用 ReadableStream 实现流式传输

目标:不将整个内容加载到内存,而是以流(stream)的方式分块传输,保持低内存占用。


三、c.html():流式返回大型 HTML

✅ 正确做法:使用 ReadableStream
ts
app.get('/stream-html', (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      // 模拟生成大型 HTML(如日志、报告)
      controller.enqueue('<!DOCTYPE html><html><body>\n')
      controller.enqueue('<h1>Large Report</h1>\n')
      controller.enqueue('<ul>\n')

      for (let i = 0; i < 100_000; i++) {
        controller.enqueue(`<li>Item ${i}</li>\n`)

        // 每 1000 行让步,避免阻塞事件循环
        if (i % 1000 === 0) {
          await new Promise(resolve => setTimeout(resolve, 0))
        }
      }

      controller.enqueue('</ul>\n')
      controller.enqueue('</body></html>\n')
      controller.close()
    }
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/html; charset=utf-8' }
  })
})

注意:Hono 的 c.html() 内部是 new Response(body, { headers: { 'Content-Type': 'text/html' } }),因此你可以直接返回 Response


四、c.xml():流式返回大型 XML

✅ 流式 XML 生成
ts
app.get('/stream-xml', (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue('<?xml version="1.0" encoding="UTF-8"?>\n')
      controller.enqueue('<data>\n')

      for (let i = 0; i < 50_000; i++) {
        const item = `<record><id>${i}</id><name>User-${i}</name></record>\n`
        controller.enqueue(item)

        if (i % 500 === 0) {
          await new Promise(resolve => setTimeout(resolve, 0))
        }
      }

      controller.enqueue('</data>\n')
      controller.close()
    }
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'application/xml' }
  })
})

五、c.file():流式传输大文件(关键!)

这才是 c.file()正确打开方式:直接传入 ReadableStream

✅ 从 R2 / S3 流式下载
ts
app.get('/video', async (c) => {
  const object = await c.env.MY_BUCKET.get('huge-video.mp4')
  if (!object) return c.notFound()

  // ✅ 直接使用 object.body(ReadableStream)
  const headers = new Headers()
  object.writeHttpMetadata(headers)
  headers.set('etag', object.httpEtag)

  return new Response(object.body, {  // ← 直接传入 stream
    headers
  })
})
✅ 从本地文件系统流式读取(Node.js)
ts
import fs from 'fs'

app.get('/stream-file', (c) => {
  const fileStream = fs.createReadStream('./large-file.zip')

  return new Response(fileStream as any, {
    headers: {
      'Content-Type': 'application/zip',
      'Content-Disposition': 'attachment; filename="large-file.zip"'
    }
  })
})
✅ 生成动态大文件(如 CSV)
ts
app.get('/export.csv', (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      // CSV 头
      controller.enqueue('id,name,email\n')

      for (let i = 0; i < 1_000_000; i++) {
        const line = `${i},User-${i},user${i}@example.com\n`
        controller.enqueue(line)

        if (i % 10_000 === 0) {
          await new Promise(resolve => setTimeout(resolve, 0))
        }
      }

      controller.close()
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/csv',
      'Content-Disposition': 'attachment; filename="export.csv"'
    }
  })
})

六、性能与内存对比

方法内存占用是否阻塞适用场景
fs.readFileSync() + c.html()高(文件大小)小文件(< 10MB)
file.arrayBuffer() + c.file()高(文件大小)小文件
ReadableStream + Response(stream)低(固定小缓冲)大文件、动态生成

使用流式传输,内存占用从 O(n) 降为 O(1),极大提升可扩展性。


七、最佳实践

  1. 永远不要对大文件使用 .text().arrayBuffer().json()
  2. 优先使用 ReadableStream:无论是文件、数据库游标还是生成式内容;
  3. 设置正确的 Content-TypeContent-Disposition
  4. 支持 Range 请求(可选):实现视频/音频的拖拽播放;
  5. 添加压缩(如 gzip):进一步减少传输体积;
  6. 监控流速和错误:确保流正确关闭。

八、结论

c.html()c.xml()c.file() 本身不是问题,问题在于如何准备响应体
通过拥抱 ReadableStream,你可以:

  • ✅ 避免内存溢出;
  • ✅ 支持任意大小的文件;
  • ✅ 实现渐进式内容加载;
  • ✅ 构建高性能、可扩展的 API。

在现代 Web 开发中,流式传输是处理大内容的黄金标准。Hono 对 Response 的灵活支持,让你能轻松实现这一最佳实践。