JS 正常、Canvas 位图也画好了,为什么屏幕上还是一片黑?

一次 Firefox 159 / Windows 双显卡环境下 Accelerated Canvas2D 合成层故障的完整定位。

从「容器高度是 0」的错误直觉,到 RenderDXGITextureHost ... hr=0x80070057 的实机日志;
从「要不换个图表库吧」,到最终只改了一个参数。
本文记录全部六轮假设、三次被推翻的结论,以及一套可以复用的「渲染故障分层定位法」。


TL;DR

问题结论
现象只有 Firefox 下、只有图表区域变黑;浏览器运行很久才出现;重启 Firefox 立刻恢复
表面看像 ECharts 多实例冲突 / 容器高度塌陷 / 某个库的 bug
实际Firefox 的 GPU-backed 2D canvas 纹理在交给 WebRender 合成器时失败,与 JS、图表库、容器尺寸、数据都无关
决定性证据about:support 的 Failure Log 里 Firefox 自己写下 RenderDXGITextureHost Failed to open shared texture, hr=0x80070057
应用侧修复echarts.init(el, null, { renderer: 'svg' }) —— 一个参数,不换库、不改 option、不加依赖
浏览器侧根治about:config → gfx.canvas.accelerated = false → 重启

一句话:位图是好的,坏的是「把位图贴到屏幕上」那一步。


1. 现象:一个「只在跑久了之后才出现」的黑框

内部一套 Vue 3 + ECharts 6 的威胁情报大屏,Chrome / Edge 一切正常,Firefox 下四个图表区域整块纯黑,其余 KPI 卡片、文字、边框、地图底框都正常。

最容易让人跑偏的三个特征:

  1. 只有图表黑,不是整页黑 → 第一反应必然是「图表库出问题了」。
  2. 不是一打开就黑,是浏览器跑很久之后才黑 → 很难和「部署了新版本」「改了 CSS」关联起来。
  3. 重启 Firefox 立刻好 → 更像玄学,也更容易被当成「缓存 / 扩展 / 偶发」而放过。

环境:

项值
浏览器Firefox 159.0(Windows 10 x64)
前端Vue 3 + ECharts 6.1.0,canvas 渲染器(ECharts 默认)
显卡双显卡:NVIDIA RTX A2000 8GB(Active)+ Intel UHD Graphics
屏幕3440×1440 @ 60Hz,DPR 1
合成器WebRender Layer Compositor
ACCELERATED_CANVAS2Ddefault / available

服务端尝试过用 headless Firefox 复现,但那台机器缺 libgtk-3.so.0 等 GTK3 图形栈、且容器里 unshare(CLONE_NEWPID) 被拒,Firefox 起不来。后面所有 Firefox 侧的关键证据,都来自实机截图、about:support 与一个专门的探针页,不是我在服务端「猜」出来的。这一点在后半段很关键。


2. 排查时间线:六轮假设,三次被推翻

阶段假设做法结果
1多个 ECharts 实例互相冲突 / 容器 0 高度纯静态单页放 6 个 ECharts 实例 + 开关❌ 多实例无冲突;但确实复现了「容器 0 高度 → 一片黑」
2是 ECharts 这个库的问题同页并列 4 个开源库 + 2 种原生实现✅ 分界出现:4 个 canvas 系全黑,3 个 SVG 系全正常
3页面 JS 根本没执行零依赖分层自检页(L0–L7)❌ Gate 绿,全层正常
4是 canvas 位图没画出来采样 getImageData 计算着墨率❌ 位图着墨 32~48%,toDataURL 有内容
5换库能解决同一个 ECharts、同一份 option,只改 renderer✅ 切 SVG 后全部正常 → 不用换库
6是静态配置 / 驱动不兼容结合「重启恢复」线索 + Bugzilla + 实机日志✅ 定性为 GPU 进程运行期资源退化,并由 Firefox 自己的日志证实

下面挑三个真正的转折点展开。


阶段 1:一个「看起来完全成立」的误判

先做了一个纯静态页:没有任何框架、路由、接口、大屏布局,只有 6 个 ECharts 实例和几个开关(0 高度容器、模拟大屏布局、ResizeObserver、延迟 init)。

结果非常「漂亮」地复现了黑框:当把布局模拟成修复前的写法时,6 个图表容器全部变成 N × 0,6/6 全黑,resize() 和 ResizeObserver 都救不回来。自动结论甚至打印出「检出 6 个异常图表:容器 0 尺寸 → 黑框」。

这一步差点让排查结束在这里。 但回头核对线上产物,发现大屏的 CSS 早就有:

.chart {
  flex: 1;
  width: 100%;
  min-height: 60px;   /* 高度链有下限,不会塌陷 */
}

而且 ResizeObserver 里也有 if (r.width > 0 && r.height > 0) c.resize() 的兜底。也就是说:lab 页模拟的是修复之前的大屏,不是线上现状。0 高度确实能造成一片黑,但不是这次的原因。

顺带纠了一个逻辑错误:当 6 个实例全部挂掉时,样本里根本没有「未受影响的实例」,此时推不出「各实例互不影响、所以不存在冲突」。隔离性证据只能来自「只压其中 2 个、其余 4 个正常」那组对照。页面的自动结论原来在全异常时也输出隔离结论,会误导人——已改成三分支。

教训:能复现「同类现象」不等于找到了「同一个根因」。复现脚本必须和线上产物逐字核对。


阶段 2:7 张卡片,把问题从「库」压到「渲染技术」

既然不是布局,就做了一个「跨图表库 × 渲染技术」对照页:同一页、同一份假数据、固定 height:200px; min-height:120px(排除 0 尺寸),并列 7 种渲染方式:

#实现技术Firefox 实机
①原生 Canvas 2Dcanvas❌ 黑
②原生 SVG(写死在 HTML 里)SVG✅ 正常
③ECharts 6.1.0(默认)canvas❌ 黑
④Chart.js 4.4.7canvas❌ 黑
⑤uPlot 1.6.31canvas❌ 黑
⑥ApexCharts 3.54.1SVG✅ 正常
⑦ECharts 6.1.0 / SVG rendererSVG✅ 正常

顶部自检:canvas 4/4 · svg 3/3 · JS 错误 0,Canvas 2D 指纹自测写入 #ff0000 能原样读回。

跨图表库 × 渲染技术对照页

这张表是整次排查的分水岭。 分界线不在「哪个库」「哪份配置」「哪个容器」,而只在于有没有调用 getContext('2d')。四个独立实现、四套完全无关的代码同时变黑,三个 SVG 实现全部正常——这已经不可能是应用层 bug。

最关键的设计是 ③ 和 ⑦:同一个 ECharts 构建、同一份 baseOption(),只差一个参数:

const c1 = echarts.init(el3)                          // ③ canvas,黑
const c2 = echarts.init(el7, null, { renderer: 'svg' })  // ⑦ SVG,正常
c1.setOption(baseOption(), true)
c2.setOption(baseOption(), true)

这就把「要不要换库」这个问题直接消灭了。换 ApexCharts 意味着重写全部图表配置;而 ECharts 自带第二个渲染器,改一个参数就行。


阶段 3:先证明「JS 到底跑没跑」

在拿到实机截图之前,还有一个更底层的可能性没排除:页面脚本根本没执行(NoScript、企业策略 javascript.enabled=false、安全扩展、脚本被网关拦掉)。如果 JS 没跑,那前面所有关于库的讨论都是空中楼阁。

于是做了一个零依赖分层自检页,从 L0 到 L7 逐层加码:

零依赖分层自检页

设计上有个小技巧值得单独说:每一层的状态都是纯 DOM 文字,且初始文案写死成失败含义(「检测中…(若一直停在这行,说明 JS 未执行)」)。顶部 Gate 默认是红字 ❌ JS 未执行,脚本一执行就改成绿字。

这样即使所有图形全黑、JS 完全不跑,光靠读文字也能定位是哪一层开始断的。层分别是:纯 HTML 文字 → CSS 色块 → 写死在 HTML 里的静态 SVG → img data URI → Canvas 画纯红并 getImageData 读回 → JS 动态创建 SVG → ECharts 最小实例 → 环境快照。

实机结果是 Gate 绿、L0–L7 全过。JS 没问题。


阶段 4:实机截图给出的决定性矛盾

真正定案的是一张 Firefox 实机截图:

  • ①③④⑤ 的自检全部是绿的——getImageData 采到着墨率 32~48%,toDataURL() 有内容,指纹正常;
  • 但屏幕上是一整块纯黑;
  • ②⑥⑦ 正常可见。

也就是说:canvas 位图确实画好了,而且能被读回来,只是合成到屏幕时变黑了。

这一句同时排除了:JS 被禁、隐私防护(RFP / CanvasBlocker 会破坏 getImageData 读回)、容器 0 尺寸、库 bug、多实例冲突、深色背景观感。

自检读的是位图,不是屏幕。自检全绿但肉眼黑 ⇒ 故障在浏览器合成层。这是整次排查里最容易被忽略、也最值钱的一条判据。


阶段 5:修复只改了一个参数

- instances[key] = echarts.init(el)
+ // Firefox 下 canvas 位图有着墨、但合成到屏幕整块纯黑(硬件加速 / WebRender 层)
+ // 同一个 ECharts 换成自带 SVG 渲染器即可正常显示,option 与数据结构不变
+ instances[key] = echarts.init(el, null, { renderer: 'svg' })

改之前先确认过兼容性:图表只用到 bar / line / map + effectScatter,没有 echarts-gl、progressive、custom、large、useDirtyRect,代码里也没有任何 getDataURL / toDataURL / canvas API 依赖。所以切渲染器是零风险的。

验证(headless Chromium,1920×1080):4 个图表全 <svg>、0 个 <canvas>,SVG 节点数 17 / 196 / 45 / 17,JS 错误 0。实机 Firefox 强刷后确认黑框消失。

没有换库,没有加依赖,没有改数据结构,没有改 CSS。


3. 根因:两条路径被分离了

用户后来补了一句关键线索:「重启 Firefox 之后这个问题也能恢复,通常在 Firefox 运行很久之后才出现」。

这句话把定性从「静态配置 / 驱动不兼容」直接改判为 GPU 进程运行期资源状态退化——重启会重建 GPU 进程、ANGLE 上下文、canvas 纹理池和共享页分配器;而配置、扩展、驱动在这期间一个字都没变。

Firefox 从 133 起在 Windows 默认开启 Accelerated Canvas2D,2D canvas 走 WebGL / ANGLE 的 GPU 纹理。于是同一个 canvas 有了两条路径:

写入像素  ──►  GPU-backed 2D canvas 纹理
                    │
                    ├──► GPU→CPU readback   ──► getImageData() / toDataURL()
                    │                           ✅ 完好(自检全绿)
                    │
                    └──► DXGI 共享纹理句柄 ──► RenderDXGITextureHost
                                            ──► WebRender 合成器贴图
                                            ❌ 失败 → 屏幕纯黑
路径状态说明
getImageData() / toDataURL()✅GPU→CPU 回读拿到完好位图
toDataURL() → <img>✅同一份位图,走普通图片合成,正常显示
willReadFrequently: true✅强制 CPU 后端,绕开 GPU 纹理
原生 / ECharts / Chart.js / uPlot 的 canvas❌屏幕合成时拿不到有效纹理
SVG(DOM / display-list)✅WebRender 直接光栅化,不占 canvas 纹理页

长时间运行后,纹理页池碎片化、共享句柄失效,或 GPU 进程异常后未正确降级,合成器就拿到一张空纹理。SVG 完全不走这条路,所以始终正常——这正是「一个参数就能修」的物理原因。

决定性证据:Firefox 自己写下的日志

回传的 about:support → Graphics → Failure Log 里:

(#0)      GP+[GFX1-]: RenderDXGITextureHost Failed to open shared texture, hr=0x80070057
(#91377)  GP+[GFX1-]: RenderDXGITextureHost Failed to open shared texture, hr=0x80070057
   … 连续 15 条同一错误 …
(#91391)  GP+[GFX1-]: RenderDXGITextureHost Failed to open shared texture, hr=0x80070057
  • hr=0x80070057 = E_INVALIDARG(HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER = 87))。
  • RenderDXGITextureHost 是 WebRender 里负责打开 DXGI 共享纹理句柄 / keyed mutex 的组件,正是上面那条失败路径的落点。
  • EGL_EXTENSIONS 里同时列着 EGL_ANGLE_d3d_share_handle_client_buffer、EGL_ANGLE_surface_d3d_texture_2d_share_handle、EGL_ANGLE_keyed_mutex —— 与失败日志指向的机制逐项对应。
  • 日志序号累计到 #91391 且末尾连续 15 条同错,是「跑久了才出现、重启即恢复」的量化注脚:重启重建 GPU 进程与 D3D 设备后恢复。

到这一步,因果链不再是推断:位图完好 + <img> / CPU 后端 / SVG 全正常 + Firefox 自己记录「打不开共享纹理」,故障被唯一地钉在「GPU-backed 2D canvas 纹理的共享句柄呈现」上。

双显卡是加重因素

GPU状态备注
NVIDIA RTX A2000 8GBActive = YesWebGPU 默认适配器走它(Dx12)
Intel UHD GraphicsActive = NoANGLE / WebGL 实际走它(D3D11)

ANGLE 在核显上创建 D3D11 共享纹理,系统 Active GPU 却是独显——跨适配器打开共享句柄本就是 E_INVALIDARG 的高发场景。WebGL Renderer 里也明确不是软件渲染(排除 RDP / 虚拟机 / SwiftShader)。


4. 两条解法,作用在不同层

层做法效果代价
应用侧ECharts 切 { renderer: 'svg' }本应用立即免疫,与用户浏览器配置无关数据量大时 SVG 节点开销高,需评估
浏览器侧about:config → gfx.canvas.accelerated = false → 重启一次性根治,所有网站的 canvas 都不再黑需用户操作;对图表性能几乎无感
浏览器侧(双显卡专项)Windows 图形设置 / NVIDIA 控制面板给 firefox.exe 固定单块 GPU避免跨适配器共享句柄笔记本上会改变功耗策略
浏览器侧(其它)gfx.canvas.accelerated.shared-page-size = 0、升级 Firefox / 显卡驱动、临时重启缓解不一定根除

应用侧先修,是为了让用户不依赖任何客户端配置就能正常看大屏;浏览器侧那一步才是把根因关掉。两者不冲突。


5. 方法论沉淀:下次遇到「图形全黑」直接抄

  1. 先做纯静态复现页,剥离框架、路由、接口、布局。变量越少,结论越硬。
  2. 做「同库同数据、只改一个变量」的 A/B。③/⑦ 那对卡片是整次排查最高效的设计:直接把「换库」从选项里划掉。
  3. 自检要区分「位图」和「屏幕」。getImageData 绿 ≠ 用户看得见;这两者之间还隔着一层合成器。
  4. 分层自检页的状态用纯文字,初始文案写死失败含义,Gate 默认红、JS 执行后变绿。图形全黑时,文字是唯一还能读的东西。
  5. 横向铺开多个独立实现找分界。四个库同时坏,几乎必然是它们之下的公共层;canvas 4 黑 / svg 3 正常 这种整齐的分界,比任何单点日志都更能定位。
  6. 能复现「同类现象」≠ 找到「同一根因」。阶段 1 的 0 高度就是陷阱,必须和线上产物逐字核对。
  7. 最后一定要拿到浏览器自诊断数据:about:support 的 Graphics / Failure Log / Decision Log,比猜测和论坛帖子可靠得多。
  8. 诊断页的文案也是交付物的一部分。见下节。

6. 我踩的两个坑,比技术结论更值得记

坑一:把「能复现」当成「找到了」。 阶段 1 的 6 个 0 高度容器逐字复现了黑框,但那是 lab 页模拟的旧 CSS,线上早修了。另外自动结论在「全挂」时仍输出「各图表互不影响」,属于逻辑错误——全挂样本里根本没有对照组。

坑二:探针页的文案把用户带沟里了。 我做的 GPU 探针页要用户「勾出哪几项变黑」,但 warnbar 写了「勾下面 4 个复选框」,结果对方把 4 项全勾了,自动结论错判成「4 项全黑 → 浏览器整体渲染问题」。而他在报告末尾明明写了「只有第一个是黑屏」。

这不是用户操作问题,是我的交付缺陷:一句有歧义的提示,差点让已到手的决定性结论被误读成另一个方向的故障。后来改成「只勾实际变黑 / 空白的项,正常显示的不要勾」,并加了 4 个一键预设(「只有 A 黑(最常见)」/「A + C 黑」/「全部正常」/「4 项全黑」),全勾分支先弹出「请确认是不是把 4 个框都勾了」的警示,报告里也加一行 [勾选规则]。

诊断工具本身也会产生错误数据,而且它长得越像「自动结论」,越容易被无条件相信。


7. 关于技术选型:不是「以后尽量用 SVG,不用 Canvas」

这次很容易被总结成「Canvas 不靠谱,以后都用 SVG」,但这个结论是过头的:

  • 本次是 Firefox 特定版本 + Windows 双显卡 + 长会话下的 GPU 合成层实现缺陷,Chromium / Safari 不复现,重启即恢复。不是 Canvas 技术路线的问题。
  • ECharts 官方 handbook 明确写:数据量 > 1k(经验值)、较多交互时建议 Canvas;SVG 的优势是内存占用低、浏览器缩放不模糊、同页多实例更稳;炫光尾迹特效、带混合效果的热力图等仍依赖 Canvas。v5.3.0 用 Virtual DOM 重构后 SVG 性能提升 2~10 倍,但没改变这个分工。
  • 本次大屏切 SVG 后 4 张图共 275 个 SVG 节点(17 / 196 / 45 / 17),远在 SVG 甜区,切得对;但换成几万点的散点图,SVG 会把 DOM 撑爆。

更合理的做法是:中小数据量的业务图表把 SVG 设为默认值,把 renderer 做成可配置项或按数据点阈值自动选择,option 和数据结构完全不动。这样既免疫这类浏览器合成 bug,又给大数据量页面留着 Canvas 的后路。

一句话:「SVG 作默认、Canvas 留后路、renderer 可切换」,而不是站队。


参考


本文只讨论浏览器渲染链路,示例页均为纯静态自测页;文中平台、域名、主机与业务数据均已做脱敏或替换。

标签: none

添加新评论