欢迎光临
我们一直在努力

Blob 图片去哪了?——DevTools 里看得见,`blob-internals` 找不到的秘密

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(终极武器)

如果前两种方式都不够用,可以拍摄内存快照:

  • 打开 DevTools → Memory 面板
  • 选择 Heap snapshot
  • 点击“Take snapshot”
  • 在结果中搜索 Blob 或 Image
  • 通过快照可以确认 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 是否真的被释放?

  • 在 DevTools 里手动触发垃圾回收: Performance 面板 → 垃圾桶图标 🗑️(Collect garbage)
  • 拍一个 Heap snapshot,搜索 Blob,看数量是否为 0(或只剩下必要的)
  • ✅ 什么时候该 revoke?

    场景建议
    图片用于 <img> 且用户可能会重复查看 等到图片被移出 DOM 或用户离开页面后再 revoke(例如结合 IntersectionObserver)
    图片仅用于一次下载(如导出报表) click() 后立即 revoke
    实现了 LRU 缓存 淘汰时 revoke,但淘汰阈值要合理,避免刚进入视口的图片被过早淘汰
    页面关闭 / 刷新 不用手动 revoke,浏览器会自动清理

    ✅ 调试时临时调大缓存

    const MAX_CACHE_SIZE = process.env.NODE_ENV === 'development' ? 999 : 30;

    这样在开发阶段可以轻松在 blob-internals 里看到所有图片。


    六、总结

    工具能看到的 Blob 图片生命周期阶段
    Application → Images ✅ 已渲染的所有图片 渲染后(即使已 revoke)
    chrome://blob-internals ✅ 未被 revoke 的 Blob 映射 从 create 到 revoke 之间
    Heap snapshot ✅ 所有存活的 Blob 对象 只要未被 GC

    记住一句话:

    revokeObjectURL 会让 blob-internals 里的记录消失,但已经画在屏幕上的图片会继续存在 —— 浏览器选择的是“不闪屏”,而不是“绝对一致”。

    你的 LRU 缓存设计没有错,错的是对调试工具预期的偏差。希望这篇文章能帮你,以及所有被同样问题困扰的开发者,少走一次弯路。


    📌 相关阅读

    • MDN: URL.createObjectURL() 在这里插入图片描述
    赞(0)
    未经允许不得转载:171主机测评 » Blob 图片去哪了?——DevTools 里看得见,`blob-internals` 找不到的秘密
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址