Appearance
Echarts 动态图表导出 GIF
ECharts 图表的入场动画(数据更新时的渐变、过渡)在交互式页面上效果很好,但想要导出为 GIF 展示给他人时,问题就来了。
方案:定时截取 + 帧合成
ECharts 提供 getDataURL() API,可以对当前画布做快照,返回 Base64 编码的 PNG:
js
const snapshot = chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff',
});基于这个 API 的 GIF 导出思路:在动画期间定时截取画布快照,收集帧序列,再用第三方库合成为 GIF。
伪代码:
js
const frames = [];
const timer = setInterval(() => {
frames.push(chartInstance.getDataURL({ type: 'png' }));
}, 50); // 目标 20fps
// 动画结束后
gifshot.createGIF({ images: frames, gifWidth: 800, gifHeight: 600 }, (obj) => {
if (!obj.error) downloadGif(obj.image);
});方案硬伤
这个方案在两个方面有根本性的精度限制:
1. 时间精度:setInterval 的最小间隔
setInterval(fn, 50) 的名义间隔是 50ms(20fps)。但浏览器对定时器的实际精度有下限:
- 非活动标签页(background tab)中,
setInterval被节流到最低 1000ms(1fps) - 即使在活动标签页中,
setInterval的实际触发间隔也不稳定——宏任务队列中前面的任务会推迟定时器的执行 - 嵌套超过 5 层的
setTimeout/setInterval有 4ms 的最小延迟
结果:帧率既不可控也不稳定。动画中关键的过渡帧可能完全被跳过,生成的 GIF 看起来"跳跃"而非流畅。
2. 编码效率:Base64 的中间态膨胀
getDataURL 返回 Base64 编码的 PNG。一个 800×600 的图表快照,PNG 约 50-200KB,Base64 编码后膨胀 33% 到约 70-270KB。如果动画持续 3 秒,目标 20fps,需要 60 帧——帧数据在内存中占用约 10-20MB。
gifshot.js 在浏览器主线程上执行 GIF 编码(颜色量化 + LZW 压缩),对 60 帧 800×600 的图片序列,编码时间可能在 5-15 秒——期间页面完全无响应。
替代方案选择矩阵
| 方案 | 帧率精度 | 画质 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 定时截取 + gifshot | ❌ 不稳定 | ❌ 256 色 GIF | 低 | 无。本方案不可用于生产 |
| Puppeteer 逐帧渲染 | ✅ 精确 | ✅ 可输出视频 | 中 | 需要精确控制和高质量输出的自动化场景 |
| MediaRecorder API | ✅ 60fps | ✅ 视频编码 | 中 | 浏览器端实时录制 |
| 录屏工具(OBS/ScreenFlow) | ✅ 60fps | ✅ 取决于编码设置 | 极低 | 一次性演示、不需要程序化导出 |
Puppeteer 方案
在 Node.js 中用 Puppeteer/Playwright 启动 headless Chrome,加载 ECharts 页面,在动画期间逐帧截图,用 ffmpeg 合成视频/GIF:
js
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/chart');
// 触发动画
await page.click('#animate-btn');
// 逐帧截图
const frames = [];
const startTime = Date.now();
while (Date.now() - startTime < 3000) {
frames.push(await page.screenshot({ type: 'png' }));
await new Promise(r => setTimeout(r, 33)); // 30fps
}
// 用 ffmpeg 合成(命令行调用)
// ffmpeg -framerate 30 -i frame_%d.png output.gifPuppeteer 方案的帧间隔精确(setTimeout 没有浏览器标签页节流的限制),画质不受 GIF 256 色调色板限制(可输出 H.264 视频)。代价是需要 Node.js 服务端环境和额外的依赖(Chromium ~300MB)。
MediaRecorder API
从 <canvas> 直接录制视频流:
js
const stream = canvas.captureStream(60); // 60fps
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
// download or display
};
recorder.start();
// 动画结束后
recorder.stop();MediaRecorder 使用浏览器的硬件视频编码器,帧率精确到 60fps,编码在主线程之外(GPU/硬件编码器)。这是浏览器端录制的首选方案,局限性是输出格式为 WebM(VP8/VP9),部分平台预览不便,且 Safari 支持滞后(Safari 14.1+)。
结论
"定时截取 + gifshot"方案在所有维度上都不满足生产要求。根据场景选择替代方案:
- 需要程序化导出且可控 → Puppeteer + ffmpeg
- 浏览器端实时录制 → MediaRecorder API
- 一次性演示/分享 → OBS 录屏
