Base64 Files
深度指南

大文件为什么不适合 Base64?
什么时候才该用?

Base64 是把文件变成文本字符串的便捷方式,但它有明显的代价:体积增大、内存翻倍、浏览器卡顿。这篇文章解释背后的原因,并帮你判断什么时候该用 Base64,什么时候该换其他方案。

Base64 为什么会让文件变大

Base64 的核心原理是:每次取原始数据的 3 个字节(24 bit),拆成 4 组 6 bit,再用 64 个可打印 ASCII 字符(A–Z, a–z, 0–9, +, /)各表示一组。

结果就是:3 个字节变成 4 个字符,每个字符占 1 字节,体积增大到原来的 4/3 ≈ 133%。

编码示例

原始字节
4D616E
3 字节 = 24 bit
Base64 字符
TWFu
4 字符 = 4 字节

+33%

体积增幅

如果原始数据字节数不是 3 的倍数,会补 = 填充到 4 的倍数,实际增幅略高于 33%。

举例:一张 5 MB 的图片,Base64 后约 6.7 MB;一个 100 MB 的视频,Base64 后约 133 MB——比原文件多传 33 MB。

浏览器转换大文件时为什么会卡

最常见的写法是 FileReader.readAsDataURL(file)。问题在于这个 API 的工作方式:

  1. 1
    全量读入内存整个文件一次性被读成 ArrayBuffer,例如 50 MB 文件就占用 50 MB 内存。
  2. 2
    主线程上编码Base64 编码在 JavaScript 主线程上执行,期间浏览器无法响应用户交互,页面卡死。
  3. 3
    构建超长字符串编码结果是一个巨大的字符串(50 MB 文件 ≈ 67 MB 字符串),JavaScript 的字符串是不可变的,构建过程涉及大量内存分配和复制。
  4. 4
    内存峰値翻倍原始 ArrayBuffer + Base64 字符串同时存在于内存,瞬时内存占用约为原文件的 2.3 倍 50 MB 的文件可以瞬间吃掉约 115 MB 内存。

如何避免卡顿

将 Base64 编码迁移到 Web Worker。主线程通过 postMessage(buffer, [buffer]) 将 ArrayBuffer 零拷贝转移给 Worker,编码在后台线程完成,结果再传回主线程。整个过程不阻塞 UI,页面保持流畅。

三种方案:该选哪个?

不同场景有不同的最优解。以下是三种文件处理方案的典型适用情况:

推荐 Base64
  • CSS 背景小图(< 5 KB)
  • favicon / app icon 内联
  • HTML 邮件内嵌图片
  • @font-face 内嵌小字体
  • JSON API 传小附件(签名图、缩略图)
  • Data URL 生成浏览器端下载链接
推荐 multipart/form-data
  • 用户上传头像、文档到自有服务器
  • 需要服务端校验文件类型和大小
  • 和表单字段一起提交
  • 通过 XMLHttpRequest / Fetch 上传
  • 文件大小不确定(几 KB 到几百 MB)
  • 需要上传进度条(XHR progress 事件)
推荐对象存储
  • 图片、视频、音频需要长期保存
  • 需要 CDN 加速或全球访问
  • 前端预签名 URL 直传(不过服务器)
  • 大文件(> 10 MB)分片上传
  • 多用户共享同一文件
  • 需要按量付费控制成本

文件大小参考边界

没有绝对的规定,但以下边界被大多数开发者遵循:

文件大小推荐方案说明
< 5 KBBase64 内联CSS 背景、图标、邮件图片,节省一次 HTTP 请求
5 KB – 1 MBBase64 或 multipart取决于是否需要内联;图片较大时 multipart 更省带宽
1 MB – 10 MBmultipart/form-dataBase64 体积增幅明显,上传耗时增加,浏览器内存压力变大
> 10 MB对象存储直传前端预签名 URL 直传 S3/OSS,不占用应用服务器带宽
> 100 MB对象存储分片上传断点续传、并发分片,避免单次请求超时

用 Web Worker 处理大文件 Base64

如果你确实需要在浏览器端对较大文件做 Base64 编码,把耗时操作放到 Worker 可以避免 UI 冻结:

base64.worker.js
self.onmessage = function(e) {
  const buffer = e.data            // ArrayBuffer(零拷贝转移)
  const bytes  = new Uint8Array(buffer)
  let binary   = ''
  const CHUNK  = 8192
  for (let i = 0; i < bytes.length; i += CHUNK) {
    binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK))
  }
  const base64 = btoa(binary)
  self.postMessage(base64)
}
主线程调用
const worker = new Worker('/base64.worker.js')

input.addEventListener('change', async (e) => {
  const file   = e.target.files[0]
  const buffer = await file.arrayBuffer()

  // Transferable:零拷贝,buffer 所有权移交给 Worker
  worker.postMessage(buffer, [buffer])
})

worker.onmessage = (e) => {
  const base64 = e.data             // 编码结果
  console.log('data:...;base64,' + base64)
}

postMessage(buffer, [buffer]) 第二个参数是 Transferable 列表,ArrayBuffer 的所有权被零拷贝移交给 Worker,主线程的 buffer 变为空(长度为 0),避免内存翻倍。

快速决策:一句话判断

Q
需要把图片写死在 CSS 或 HTML 里?用 Base64。
Q
用户要把文件发给你的服务器?用 multipart/form-data。
Q
文件需要存储、复用、CDN 分发?用对象存储 + 预签名 URL。
Q
文件只在浏览器端处理,不上传?用 Blob URL(URL.createObjectURL),不用 Base64。
Q
API 要求传 Base64,但文件很大?考虑改用 multipart 或 presigned URL,和 API 方协商。
Q
确实要 Base64 且文件 > 5 MB?用 Web Worker,把编码放到后台线程。

相关工具

常见问题

Base64 编码后文件为什么会变大?

Base64 将每 3 字节编码为 4 个 ASCII 字符,体积变为原来的 4/3,约增大 33%。如果原始数据不是 3 的倍数,末尾会补 = 填充,实际增幅略高于 33%。

浏览器转换大文件为 Base64 时为什么会卡死?

FileReader.readAsDataURL() 在主线程全量读取并编码文件,期间无法响应 UI 事件。加上原始数据和 Base64 字符串同时存在内存,峰値占用约为文件大小的 2.3 倍。解决方法是使用 Web Worker。

Blob URL 和 Base64 Data URL 有什么区别?

Blob URL(URL.createObjectURL)是浏览器内部的临时引用,不复制数据,内存占用极低,适合在浏览器内显示或下载文件。Base64 Data URL 是真实的文本字符串,可以写入 HTML/CSS/JSON,适合需要内联的场景,但体积更大。

什么情况下 multipart/form-data 比 Base64 好?

需要上传到服务器时,multipart 以流式传输原始二进制,不增大体积,服务端可边接收边写盘,支持进度条,适合任意大小的文件。Base64 只适合小文件内联或 JSON API 传附件。

Web Worker 的 Transferable 是什么?

调用 postMessage(buffer, [buffer]) 时,ArrayBuffer 的所有权被零拷贝移交给 Worker,主线程的变量立即变为空(byteLength 为 0)。相比拷贝,Transferable 既快又省内存,是处理大文件时的推荐做法。

对象存储预签名 URL 是怎么工作的?

后端用 AWS SDK / OSS SDK 生成一个带签名、有时效的上传 URL,前端拿到后直接用 PUT 请求把文件传给存储服务,绕过应用服务器。文件不占用服务器带宽和内存,适合大文件上传。