Data URI 与 Blob URL:图片该用哪个
2026-09-30 · 1293 词
当你把图片字节拿在内存里、想把它显示出来时,有两种方式可以不经过服务器往返就塞进 img 元素。它们经常被当成可以互换,其实不能:一个是可携带但昂贵的文本编码,另一个是廉价的引用,只在页面活着的时候存在。
短版本
- Data URI ——
data:image/png;base64,iVBOR...—— 字节被编码成文本,嵌在 URL 自身里。自包含、可携带、可序列化,大约大 33%,并且以字符串形式活在 DOM 里。 - Blob URL ——
blob:https://example.com/5f2a...—— 指向浏览器内存里一个Blob的引用。不编码、没有体积惩罚,但只存在于创建它的那份文档里。
// data URI:字节就在字符串里
img.src = 'data:image/png;base64,' + base64Payload;
// blob URL:字节留在 Blob 里,URL 只是个句柄
const url = URL.createObjectURL(blob);
img.src = url;
// 之后图片不用了:
URL.revokeObjectURL(url);
各自的成本
data URI 那 33% 是结构性的,不是实现细节:Base64 把 3 字节装进 4 个字符,而图片字节本身已经压缩过,所以 gzip 拿不回来。一张 300 KB 的照片变成 400 KB 的字符串,而在 JavaScript 里这个字符串以 UTF-16 存储 —— 每字符 2 字节 —— 所以它大约占 800 KB 内存,还是在你依然持有的原始字节之外。
blob URL 的成本基本为零。浏览器只保留一份字节,就在你已经创建的那个 Blob 里,而 URL 只是一个指向它的短字符串。没有编码步骤,没有解码步骤,也没有第二份拷贝。
这个差别本身就能决定大部分场景。如果载荷很大或是临时的 —— 用户刚选中文件的预览、生成的图片、解码出来的 Base64 载荷 —— blob URL 就是正确答案。
各自能做什么
在可携带性上,权衡走向另一边,而这里的选择就不再是体积问题了。
data URI 是一个值。你可以把它放进 JSON 响应、写进 HTML 文件、存进 localStorage、粘进邮件、嵌进 CSS,或者发给同事的聊天消息里。它能穿越序列化、页面刷新和进程重启,因为需要的一切都在那个字符串里。
blob URL 是一个句柄。它的作用域限于创建它的文档:在新标签页打开同一个 URL,它毫无意义;把它存成文件,它是一个死引用;刷新页面,上一个文档创建的所有 blob URL 全部失效。它也不能跨源使用。
JSON.stringify({ src: 'blob:https://example.com/5f2a...' }); // 序列化没问题
// ……但对读到它的人毫无意义
没人注意到的内存泄漏
blob URL 必须被释放。每一次 createObjectURL 调用都会在内存里钉住一个 Blob,直到页面销毁或者你调用 revokeObjectURL。在一个每次按键都刷新的预览里 —— 本站解码器用的正是这个模式 —— 忘记释放意味着你每粘贴一次就积累一张被钉住的图片:
let currentUrl = null;
function show(blob) {
if (currentUrl) URL.revokeObjectURL(currentUrl); // 释放上一个
currentUrl = URL.createObjectURL(blob);
img.src = currentUrl;
}
释放一个 <img> 已经加载完的 URL 是安全且正确的:图片保留它已解码的像素,底层的字节被释放。但在图片正在加载时释放,在部分浏览器里会取消这次加载,所以要在替换时释放旧 URL,而不是释放你刚刚赋值的新 URL。
同样的泄漏在 data URI 上也存在,只是形式更不显眼:一个长期挂在 DOM 属性里的长字符串,是只有元素消失时才会释放的内存。一个 400 KB 的字符串被设为 img.src 然后被替换,浏览器可以自由释放它,但你自己变量里的那个字符串,直到你丢掉引用才会消失。
实践中各自赢在哪里
以下情况用 blob URL:
- 预览一个文件或一段解码后的载荷,尤其是超过几百 KB 的时候;
- 图片是生成的(canvas、
createImageBitmap、解码后的 Base64 载荷)而且生命周期由你控制; - 你要下载它 ——
URL.createObjectURL加download属性是不经服务器保存字节的标准做法; - 同一份字节要显示多次,因为 URL 可复用,而 data URI 会在每个属性里重复一份。
以下情况用 data URI:
- 这个值必须在当前文档之外存活 —— JSON、
localStorage、邮件模板、HTML 文件; - 你要把一个小资源嵌进 CSS 或 HTML 文档,而那里再发一次请求比这些字节更贵;
- 你产出的东西要让人复制粘贴 —— 这正是一个 Base64 转换器的全部前提。
两者都不用,当: 你已经有一个真实 URL。通过 HTTP 提供的对象或文件会被缓存、可流式传输,浏览器显示它的成本为零。把字节编码成 Base64 来省一次请求,只在很小的小资源上划算 —— 这条线在哪,见 为什么 Base64 会让页面大 33%。
还有一种情况值得点名,因为人们是为了解决它才去找 data URI 的:把字符串送进一个只接受文本的地方 —— JSON 字段、配置文件、邮件模板。如果图片很大,blob URL 和 data URI 都不是正确答案;正确答案是一个真实 URL 加一个文本引用。而如果你是在手工产出那个字符串而不是在代码里,图片转 Base64 的存在意义就是为你设想的消费方输出正确的外壳。
关于反方向的解码也补一句,因为它出现在同一段代码路径里:如果你手里是 Base64 字符串而不是 Blob,你可以完全绕开 atob 直接拿到 blob URL —— await (await fetch(dataUri)).blob() 会在内部完成解码,交给你一个 createObjectURL 能接受的东西。这是从粘贴字符串到渲染预览的最短路径,本站解码器就是这么做的;Base64 转图片 在展示结果的同时,还会验证这些字节是不是头里声称的那种。
唯一值得记住的安全提示
这两种方案本身都不执行脚本:<img> 元素不会运行 SVG 里的脚本,无论来源是 data URI 还是 blob URL。风险出现在资源不再是图片的时候 —— 把顶层文档导航到一个 image/svg+xml data URI,或者把用户提供的字节交给一个会把它当标记处理的东西。把规则保持得狭窄而具体:图片就放进 img,而 blob: URL 是你把"已经信任的内存字节"交给浏览器的方式。
有一个 CSP 细节会在两个方向上同时踩到人。data URI 需要放行 img-src data:,blob URL 需要放行 img-src blob:。一个写着 img-src 'self' 的页面会把两者都拦掉 —— 安静地拦掉,只用一条控制台违规来解释。如果你在严格 CSP 的站点上加入其中任何一种技术,请同时加上对应的来源表达式,否则你会花一整个下午去调试一段从来就没坏的 Base64。