URL.createObjectURL(blob) 返回的短字符串不是文件内容,而是浏览器管理的一条资源映射。字符串离开作用域,不等于这条映射立即失效。长期运行的图片、音频或压缩工具如果不断生成新 URL,就需要在旧结果不再使用时撤销它们。
短 URL 为什么能留住大资源?
可以把关系理解为:界面持有 URL 字符串,浏览器的 URL 注册记录关联 Blob,Blob 关联底层数据。底层数据可能在内存或其他浏览器管理的存储里,不能根据 URL 字符串只有几十个字符就推断成本很小。
对同一个 Blob 调用两次 createObjectURL,会得到两个独立的 URL;撤销其中一个不会撤销另一个。它们不一定各复制一份 Blob 字节,但每条未释放的映射都能延长资源可用时间。业务上应为每个结果保存一个明确的 URL,而不是每次渲染都创建新的。
revoke 做了什么,又没有做什么?
| 操作 | 含义 | 不代表什么 |
|---|---|---|
| URL.revokeObjectURL(url) | 撤销这一条 URL 映射 | 不删除用户已经下载的文件 |
| 释放 Blob 的 JS 引用 | 让一条 JS 可达路径消失 | 不自动撤销另一个 Object URL |
| 移除 img / video | 结束当前界面的消费 | 不保证所有原生缓存立即回收 |
| 关闭文档 | 浏览器通常清理所属 Object URL | 不能替代长会话里的主动清理 |
revoke 后不要再用这个地址开始新读取。它不是强制垃圾回收指令,也不是“立即降低任务管理器数字”的承诺:其他 Blob 引用、已解码像素、仍在消费的资源或浏览器缓存都可能继续存在。排查时要看多轮操作后的趋势,而不是只看 revoke 下一行的数值。
给预览一个明确的所有者
下面示例让同一结果拥有图片和下载链接,返回一个幂等清理函数。示例输入是 PNG Blob;集成时在结果替换、清空或视图卸载时调用它,并在替换前处理旧结果。不要把最后的清理调用紧跟在创建后面,否则图片还没读到数据,URL 就已经无效。
function mountBlobPreview(blob: Blob, host: HTMLElement): () => void {
const url = URL.createObjectURL(blob);
const image = document.createElement('img');
const download = document.createElement('a');
image.alt = 'Generated image preview';
image.src = url;
download.href = url;
download.download = 'result.png'; // This example expects a PNG Blob.
download.textContent = 'Download PNG';
host.append(image, download);
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
image.removeAttribute('src');
download.removeAttribute('href');
image.remove();
download.remove();
URL.revokeObjectURL(url);
};
}
// Keep this disposer with the current result.
const disposePreview = mountBlobPreview(pngBlob, resultContainer);
// Call disposePreview() when this result is retired or the view unmounts,
// not immediately after mounting or starting a download.React 中同样应在 effect 内创建 URL,并让该次 effect 的 cleanup 只撤销自己创建的 URL。不要在 render 或 useMemo 中调用 createObjectURL 作为无清理的副作用;开发模式下重复挂载也能暴露生命周期错误。异步生成结果时要检查任务 ID:被替换的旧任务即使晚到,也不能把过期 URL 写回界面。
为什么不能在 img.onload 里总是立即释放?
加载完成表示图片已被读取,不表示用户不再需要原始地址。如果用户还要右键保存、在新标签打开,或多个元素共享这个 URL,提前撤销会让后续操作失败。用于一次性解码、确定没有后续消费者时可以结束生命周期;持续展示的预览则通常保留到移除。视频还可能继续读取或跳转,不能照搬单张图片的时机。
下载之后等几秒再释放就可靠吗?
a.click() 不是下载完成事件,浏览器也没有通用的 anchor 下载完成回调。紧跟 click 撤销可能和资源消费产生竞态;setTimeout 延迟一秒或一分钟都只是经验策略,不是完成证明。对于结果页,保留 URL 让用户重试下载,在结果退役时再清理,并在目标浏览器测试开始下载后切换结果的行为。
若业务需要精确知道写入是否结束,可在支持环境中使用用户授权的文件写入流并等待关闭成功;这与普通下载链接是不同的交互和兼容性条件。不要为了省一个资源引用,把用户仍可能使用的下载入口变成失效链接。
如何检查清理有没有漏?
- 在开发环境记录每个结果的创建和撤销次数,不记录 Blob 的敏感内容。
- 连续生成、替换、清空十轮,确认已退役结果的 URL 都有撤销记录,活动结果数量有上限。
- 检查预览、下载、右键保存、取消和离开页面后的行为,确认没有过早撤销。
- 观察进程内存是否趋于稳定;若仍增长,继续检查 state 中的 Blob、ImageBitmap、Canvas 与 Worker,而不是反复调用 revoke。