JS 正常、Canvas 位图也画好了,为什么屏幕上还是一片黑?
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 卡片、文字、边框、地图底框都正常。
最容易让人跑偏的三个特征:
- 只有图表黑,不是整页黑 → 第一反应必然是「图表库出问题了」。
- 不是一打开就黑,是浏览器跑很久之后才黑 → 很难和「部署了新版本」「改了 CSS」关联起来。
- 重启 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_CANVAS2D | default / 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 2D | canvas | ❌ 黑 |
| ② | 原生 SVG(写死在 HTML 里) | SVG | ✅ 正常 |
| ③ | ECharts 6.1.0(默认) | canvas | ❌ 黑 |
| ④ | Chart.js 4.4.7 | canvas | ❌ 黑 |
| ⑤ | uPlot 1.6.31 | canvas | ❌ 黑 |
| ⑥ | ApexCharts 3.54.1 | SVG | ✅ 正常 |
| ⑦ | ECharts 6.1.0 / SVG renderer | SVG | ✅ 正常 |
顶部自检: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=0x80070057hr=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 8GB | Active = Yes | WebGPU 默认适配器走它(Dx12) |
| Intel UHD Graphics | Active = No | ANGLE / 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. 方法论沉淀:下次遇到「图形全黑」直接抄
- 先做纯静态复现页,剥离框架、路由、接口、布局。变量越少,结论越硬。
- 做「同库同数据、只改一个变量」的 A/B。③/⑦ 那对卡片是整次排查最高效的设计:直接把「换库」从选项里划掉。
- 自检要区分「位图」和「屏幕」。
getImageData绿 ≠ 用户看得见;这两者之间还隔着一层合成器。 - 分层自检页的状态用纯文字,初始文案写死失败含义,Gate 默认红、JS 执行后变绿。图形全黑时,文字是唯一还能读的东西。
- 横向铺开多个独立实现找分界。四个库同时坏,几乎必然是它们之下的公共层;
canvas 4 黑 / svg 3 正常这种整齐的分界,比任何单点日志都更能定位。 - 能复现「同类现象」≠ 找到「同一根因」。阶段 1 的 0 高度就是陷阱,必须和线上产物逐字核对。
- 最后一定要拿到浏览器自诊断数据:
about:support的 Graphics / Failure Log / Decision Log,比猜测和论坛帖子可靠得多。 - 诊断页的文案也是交付物的一部分。见下节。
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 可切换」,而不是站队。
参考
- ECharts Handbook:使用 Canvas 或者 SVG 渲染
- Bug 1910138 — Enable Accelerated Canvas2D on Windows(Firefox 133 起默认开启)
- Bug 1939995 — Canvas elements not shown(Windows 10 / FF133,
gfx.canvas.accelerated=false可恢复) - Bug 2080436 — Canvas draws with putImageData never painted(
getImageData/ 截图正常、只有屏幕上空白;原文 "the problem is in presenting it to the compositor",根因是 shared PBO upload path。该修复针对 macOS,不覆盖本文的 Windows 路径) - Bug 2080469 — Canvas2D 纹理反馈(FF159)(workaround 含
gfx.canvas.accelerated=false、shared-page-size=0、willReadFrequently) - MDN:
HTMLCanvasElement.getContext()— willReadFrequently
本文只讨论浏览器渲染链路,示例页均为纯静态自测页;文中平台、域名、主机与业务数据均已做脱敏或替换。