Recharts 无头截图空白:clipPath 动画在 headless 里被饿死
我在给一个数据看板页面做自动化截图时撞上了一个反直觉的坑:用 chrome-headless-shell + CDP 截出来的图,坐标轴、网格线、图例全都正常,就是图表的数据区域——柱子、折线、面积填充——一片空白。切到真实浏览器打开,完全正常。
更诡异的是,同一批截图任务里,另一个页面的图表截得好好的。难道是我的图表组件有问题?
现象
- headless 截图中,AreaChart / BarChart 的坐标轴刻度和图例完整,Y 轴值域也确实对应了真实数据(比如 0–600 而不是全零),说明数据确实绑进去了
- 但绘图区完全空白,看不到任何柱体、折线或面积色块
- 同一次 headless 运行,另一个走异步加载数据的图表页面截图完全正常
- 在真实 Chrome 里打开,所有图表都正常
根因
Recharts 的系列元素在挂载时会通过 react-smooth 播放一段入场动画:一个 SVG <clipPath> 矩形从宽度 0 逐帧展开到完整宽度,驱动方式是 requestAnimationFrame。
问题出在 headless 环境的首帧时序上:当图表以同步静态数据方式在初始渲染时挂载,rAF 回调在截图时尚未被调度执行,<clipPath> 的矩形宽度仍然是 0——系列的 <path> 几何数据已经写好了,颜色也正确,就是被这把宽度为零的"剪刀"裁掉了,什么都看不见。
而那个截图正常的页面,恰好是通过异步接口拉数据的——图表挂载时间被延迟了,等到截图触发时动画早已完成,所以没有问题。这个对比差异就是识别此坑的关键线索。
遇到 headless 截图空白时很容易怀疑数据没绑进去、容器宽度塌陷、颜色透明——三个方向都是错的。
真正的凶手是 SVG <clipPath> 动画在无头浏览器里的首帧永远停在宽度 0。坐标轴和图例没有入场动画,所以它们正常;数据系列有,所以被裁掉了。
用 CDP Runtime.evaluate 验证一下就清楚了:
const bar = document.querySelector('.recharts-rectangle');
bar.getBBox(); // -> {width: 103, height: 172, ...} 非零!
getComputedStyle(bar).fill; // -> "rgb(99, 102, 241)" 颜色正确!
// 几何在、颜色在、就是看不见 → 100% 是 clipPath 动画问题正解
在导航到目标页面之前,通过 CDP 模拟 prefers-reduced-motion: reduce。Recharts 会检测到此媒体查询并跳过入场动画,clipPath 直接展开到完整宽度,图表系列在首帧就完整渲染。
// CDP 方案(chrome-headless-shell)
await send("Emulation.setEmulatedMedia", {
features: [{ name: "prefers-reduced-motion", value: "reduce" }],
});
await send("Page.navigate", { url });
await sleep(5000);
// 此时截图,Area / Bar / Pie 全部正常出现// Playwright 方案
const context = await browser.newContext({ reducedMotion: "reduce" });这个修复不需要改任何组件代码,图表对真实用户完全不受影响——rAF 在真实浏览器里会正常触发,动画照常播放。
如果你想让静态导出、SSR、或打印环境也天然稳定,可以在图表封装层加 isAnimationActive={false} 或响应 prefers-reduced-motion,顺带也是一个无障碍(a11y)加分项。
await send("Emulation.setEmulatedMedia", {
features: [{ name: "prefers-reduced-motion", value: "reduce" }],
});放在 Page.navigate 之前调用。clipPath 动画会被跳过,系列几何在首帧就可见。无需改组件,不影响真实用户体验。
鉴别:和宽度塌陷问题的区分
| 症状 | 根因 |
|------|------|
| getBBox().width === 0,或 recharts 控制台打印"width(0) and height(0)" | 容器宽度塌陷,ResponsiveContainer 需要显式宽度 |
| getBBox() 非零、颜色正确、但截图空白 | 本文的坑:clipPath 动画未完成 |
一句话外卖
headless 截图里 Recharts 图表数据区空白,先查 clipPath,不是数据——截图前注入
prefers-reduced-motion: reduce即可绕过。