Blob 图片去哪了?——DevTools 里看得见,blob-internals 找不到的秘密
一张明明已经渲染出来的 blob: 图片,在 chrome://blob-internals 里却人间蒸发。 这不是 bug,而是浏览器生命周期管理的一次“完美错过”。
〇 引子:一个让人困惑的调试场景
如果你开发过 Chrome 扩展,或者处理过前端图片预览,大概率见过这样的 <img> 地址:
blob:https://web.whatsapp.com/7629f7ca-ca06-45fa-8038-3648281e7710
有一天你在调试时发现:
- ✅ 图片正常显示在 WhatsApp Web 的聊天窗口里
- ✅ DevTools → Application → Frames → Images 里也能看到这张 blob: 图片
- ❌ 打开 chrome://blob-internals,却搜不到任何对应的记录
你心里可能会嘀咕: “图片明明在内存里,为什么浏览器自己的内部页面说不认识它?”
这篇文章就来解开这个谜题,并彻底讲清楚 Blob 图片的三种查看方式 以及背后的生命周期原理。
一、Blob URL 极速回顾
在深入调试之前,先快速过一遍 Blob URL 的核心机制(详细原理可参考前文《一张跨域图的“四次换乘”》)。
#mermaid-svg-Z7zEFRhPHXE8P5IB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z7zEFRhPHXE8P5IB .error-icon{fill:#552222;}#mermaid-svg-Z7zEFRhPHXE8P5IB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z7zEFRhPHXE8P5IB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .marker.cross{stroke:#333333;}#mermaid-svg-Z7zEFRhPHXE8P5IB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z7zEFRhPHXE8P5IB p{margin:0;}#mermaid-svg-Z7zEFRhPHXE8P5IB .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster-label text{fill:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster-label span{color:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster-label span p{background-color:transparent;}#mermaid-svg-Z7zEFRhPHXE8P5IB .label text,#mermaid-svg-Z7zEFRhPHXE8P5IB span{fill:#333;color:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .node rect,#mermaid-svg-Z7zEFRhPHXE8P5IB .node circle,#mermaid-svg-Z7zEFRhPHXE8P5IB .node ellipse,#mermaid-svg-Z7zEFRhPHXE8P5IB .node polygon,#mermaid-svg-Z7zEFRhPHXE8P5IB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .rough-node .label text,#mermaid-svg-Z7zEFRhPHXE8P5IB .node .label text,#mermaid-svg-Z7zEFRhPHXE8P5IB .image-shape .label,#mermaid-svg-Z7zEFRhPHXE8P5IB .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z7zEFRhPHXE8P5IB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .rough-node .label,#mermaid-svg-Z7zEFRhPHXE8P5IB .node .label,#mermaid-svg-Z7zEFRhPHXE8P5IB .image-shape .label,#mermaid-svg-Z7zEFRhPHXE8P5IB .icon-shape .label{text-align:center;}#mermaid-svg-Z7zEFRhPHXE8P5IB .node.clickable{cursor:pointer;}#mermaid-svg-Z7zEFRhPHXE8P5IB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .arrowheadPath{fill:#333333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z7zEFRhPHXE8P5IB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z7zEFRhPHXE8P5IB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z7zEFRhPHXE8P5IB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster text{fill:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB .cluster span{color:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z7zEFRhPHXE8P5IB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z7zEFRhPHXE8P5IB rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z7zEFRhPHXE8P5IB .icon-shape,#mermaid-svg-Z7zEFRhPHXE8P5IB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z7zEFRhPHXE8P5IB .icon-shape p,#mermaid-svg-Z7zEFRhPHXE8P5IB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z7zEFRhPHXE8P5IB .icon-shape .label rect,#mermaid-svg-Z7zEFRhPHXE8P5IB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z7zEFRhPHXE8P5IB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z7zEFRhPHXE8P5IB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z7zEFRhPHXE8P5IB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
URL.createObjectURL
赋值给 img.src
查询 Blob URL Store
返回数据
解码+绘制
JS 内存中的 Blob 对象
blob:https://domain/UUID
浏览器渲染引擎
屏幕上显示图片
- blob: URL 只是一个“门牌号”,指向浏览器内存中的 Blob 对象。
- 门牌号的有效期从 createObjectURL 到 revokeObjectURL 或页面关闭为止。
- 一旦被 revoke,门牌号失效,但已经渲染出来的图片不会消失(浏览器已缓存像素数据)。
这个“失效≠消失”的特性,正是我们今天所有困惑的根源。
二、查看 Blob 图片的三种姿势
2.1 👁️ 方式一:DevTools Application 面板(最直观)
路径:F12 → Application 选项卡 → 左侧 Frames → 当前页面 → Images
这里会列出页面已经成功加载并渲染出来的所有图片,包括 http://、https:// 和 blob: 类型的资源。

优点:
- 所见即所得,不需要关心 URL 是否还“活着”
- 可以预览缩略图,方便确认内容
缺点:
- 无法知道该 Blob 是否还被引用、是否可被释放
- 看不到原始 Blob 的大小、类型等元信息
2.2 🧠 方式二:chrome://blob-internals(底层仓库)
在地址栏输入 chrome://blob-internals 并回车,会看到一张表格,列出当前浏览器进程中所有活跃的、未被 revoke 的 Blob 对象。
每条记录包含:
- Blob 的 UUID
- 类型 (MIME)
- 大小
- 引用计数(被几个 URL 或 JS 对象引用)
优点:
- 精确知道哪些 Blob 还“活着”
- 可以用来排查内存泄漏(Blob 一直不被释放)
缺点:
- 页面关闭、刷新、或调用 revokeObjectURL 后记录立即消失
- 不会显示已经渲染但已 revoke 的图片
2.3 🔬 方式三:Heap Snapshot(终极武器)
如果前两种方式都不够用,可以拍摄内存快照:
通过快照可以确认 Blob 对象是否真的被回收了,还是被某个闭包或 Map 意外持有。
三、核心矛盾:为什么 Application 看得见,blob-internals 找不到?
下面我们用一张 Mermaid 序列图 还原你遇到的那个典型场景 —— 一个带 LRU 缓存的 Chrome 扩展。
Application
chrome://blob-internals
Blob URL Store
渲染引擎
<img> 元素
Content Script (LRU Cache)
Application
chrome://blob-internals
Blob URL Store
渲染引擎
<img> 元素
Content Script (LRU Cache)
#mermaid-svg-NkzXAzjYHSHSJlJD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NkzXAzjYHSHSJlJD .error-icon{fill:#552222;}#mermaid-svg-NkzXAzjYHSHSJlJD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NkzXAzjYHSHSJlJD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NkzXAzjYHSHSJlJD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NkzXAzjYHSHSJlJD .marker.cross{stroke:#333333;}#mermaid-svg-NkzXAzjYHSHSJlJD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NkzXAzjYHSHSJlJD p{margin:0;}#mermaid-svg-NkzXAzjYHSHSJlJD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NkzXAzjYHSHSJlJD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-NkzXAzjYHSHSJlJD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-NkzXAzjYHSHSJlJD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-NkzXAzjYHSHSJlJD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-NkzXAzjYHSHSJlJD .sequenceNumber{fill:white;}#mermaid-svg-NkzXAzjYHSHSJlJD #sequencenumber{fill:#333;}#mermaid-svg-NkzXAzjYHSHSJlJD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-NkzXAzjYHSHSJlJD .messageText{fill:#333;stroke:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NkzXAzjYHSHSJlJD .labelText,#mermaid-svg-NkzXAzjYHSHSJlJD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .loopText,#mermaid-svg-NkzXAzjYHSHSJlJD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-NkzXAzjYHSHSJlJD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-NkzXAzjYHSHSJlJD .noteText,#mermaid-svg-NkzXAzjYHSHSJlJD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-NkzXAzjYHSHSJlJD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NkzXAzjYHSHSJlJD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NkzXAzjYHSHSJlJD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-NkzXAzjYHSHSJlJD .actorPopupMenu{position:absolute;}#mermaid-svg-NkzXAzjYHSHSJlJD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-NkzXAzjYHSHSJlJD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-NkzXAzjYHSHSJlJD .actor-man circle,#mermaid-svg-NkzXAzjYHSHSJlJD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-NkzXAzjYHSHSJlJD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
缓存满了,需要淘汰最旧的 url1
图片已经渲染完成
像素数据仍在页面内存中
createObjectURL(blob) → url1
blob:https://…/uuid1
img.src = url1
加载 url1
查询 uuid1 → 找到 Blob
返回数据
解码 + 绘制
显示记录 ✅
revokeObjectURL(url1)
删除 uuid1 映射
记录消失 ❌
图片继续显示
Frames → Images 仍能看到 ✅
关键结论:
- revokeObjectURL 只是删除了“门牌号 → 数据”的映射,不会清除已经送给渲染引擎的图像数据。
- 因此 chrome://blob-internals 里看不到记录,但 Application 面板里依然存在那条图片条目。
- 这不是 bug,而是浏览器的正常设计 —— 避免 revoke 导致正在显示的图片突然消失。
四、真实案例:我的 LRU 缓存扩展
我在开发 WhatsApp Web 的 Chrome 扩展时,实现了一个图片代理缓存:
const MAX_CACHE_SIZE = 30;
const imageCache = new Map<string, string>();
const lruQueue: string[] = [];
function evictOldest() {
const oldest = lruQueue.shift();
if (oldest && imageCache.has(oldest)) {
const blobUrl = imageCache.get(oldest)!;
URL.revokeObjectURL(blobUrl); // ← 关键:主动释放
imageCache.delete(oldest);
}
}
当缓存超过 30 张时,最久未使用的图片会被 revoke。此时:
- 那些已经滚动出视口的图片依然显示在页面上(因为渲染过了)
- 但在 chrome://blob-internals 里再也找不到它们
- 我一度怀疑代码有问题,直到明白上面的原理
验证方法:临时注释掉 revokeObjectURL 那一行,再刷新 blob-internals —— 所有记录都回来了。 (当然,正式环境必须恢复 revoke,否则内存会无限增长)
五、调试与最佳实践
✅ 如何确认 Blob 是否真的被释放?
✅ 什么时候该 revoke?
| 图片用于 <img> 且用户可能会重复查看 | 等到图片被移出 DOM 或用户离开页面后再 revoke(例如结合 IntersectionObserver) |
| 图片仅用于一次下载(如导出报表) | click() 后立即 revoke |
| 实现了 LRU 缓存 | 淘汰时 revoke,但淘汰阈值要合理,避免刚进入视口的图片被过早淘汰 |
| 页面关闭 / 刷新 | 不用手动 revoke,浏览器会自动清理 |
✅ 调试时临时调大缓存
const MAX_CACHE_SIZE = process.env.NODE_ENV === 'development' ? 999 : 30;
这样在开发阶段可以轻松在 blob-internals 里看到所有图片。
六、总结
| Application → Images | ✅ 已渲染的所有图片 | 渲染后(即使已 revoke) |
| chrome://blob-internals | ✅ 未被 revoke 的 Blob 映射 | 从 create 到 revoke 之间 |
| Heap snapshot | ✅ 所有存活的 Blob 对象 | 只要未被 GC |
记住一句话:
revokeObjectURL 会让 blob-internals 里的记录消失,但已经画在屏幕上的图片会继续存在 —— 浏览器选择的是“不闪屏”,而不是“绝对一致”。
你的 LRU 缓存设计没有错,错的是对调试工具预期的偏差。希望这篇文章能帮你,以及所有被同样问题困扰的开发者,少走一次弯路。
📌 相关阅读
- MDN: URL.createObjectURL()



