【图表+ECharts】day54-echarts-performance

作者:mario 发布时间: 2026-09-03 阅读量:3 评论数:0

Day 54 · ECharts 性能优化 — 10 万点的 60fps 之路

昨天的面板几十个数据点,怎么画都流畅。但工业场景的真实数据量是:10 万点传感器历史曲线、上千台设备的实时状态、百万级事件散点。数据量每上一个台阶,都有一个对应的优化武器。今天的产出是一份压测报告:亲手把 10 万点折线图从卡死调到流畅,用数据说话——这份报告本身就是面试"性能优化经验"的实证。


目录


一、先搞清楚:性能瓶颈在哪

1.1 ECharts 渲染一帧的完整链路

setOption → 数据处理(刻度/布局/坐标映射)
         → 视觉编码(逐元素生成图形属性:颜色/大小/形状)
         → 光栅化(canvas 绘制指令 → 像素)
         → (交互时)命中检测 + tooltip 更新
阶段 大数据量下的症状 对应武器
数据处理 setOption 后白屏几百毫秒才出图 progressive 渐进
视觉编码 每个点都建"可交互元素"结构 large 模式
光栅化 出图了但缩放/平移每帧 > 100ms sampling 降采样
内存 长时间运行内存持续上涨 dispose + 数据替换策略

第一步永远是打开 Performance 面板看火焰图,猜瓶颈是优化的大忌。

1.2 一个反直觉的事实

降采样的意义不止是"少画点":屏幕宽度只有 1920px,10 万个点意味着每个像素挤着 50+ 个点——画出来也是糊成一团,信息量为零。降采样不是妥协,是"反正看不见,不如不画"的工程智慧。


二、数据规模分级策略表

先背这张表,遇到数据量直接对号入座:

数据量 策略 关键配置
< 1 千 随便用,无需优化 默认即可
1 千 ~ 1 万 默认仍流畅,注意别频繁整图 setOption 局部更新
1 万 ~ 10 万 必开 sampling;折线可加 large sampling: 'lttb'
10 万 ~ 百万 sampling + large + progressive 全开 三件套
流式追加 appendData 增量 series.appendData
> 百万(离线分析) 考虑服务端聚合 or WebGL(echarts-gl) 超出本周范围

⚠️ 红线:任何"先跑起来看看"的侥幸心态在 10 万点面前都会得到教训——首次 setOption 卡死主线程 3 秒,用户直接关闭页面。


三、sampling:视觉无损的降采样

3.1 四种采样算法

const option = {
  series: [{
    type: "line",
    data: bigData,           // 10 万点
    sampling: "lttb",        // ⭐ 首选
    // 可选值:
    // 'lttb'     Largest-Triangle-Three-Bucket:保形状的降采样(下文详解)
    // 'average'  等距分桶取均值:平滑但丢峰谷
    // 'max'      分桶取最大:保留峰值(告警场景重要!)
    // 'min'      分桶取最小
  }],
};

3.2 为什么 lttb 是首选

普通等距采样(每隔 N 个取 1 个)会丢掉尖峰——温度曲线的瞬时尖刺可能正是故障信号。LTTB(最大三角形三次桶)算法的思路:

把数据分成 N 个桶(N = 目标点数),每桶选一个"与前后关键点组成三角形面积最大"的点
→ 面积大 = 对曲线形状贡献大 → 尖峰必被保留

视觉结果:降采样后的曲线和原始曲线肉眼几乎不可分辨,但点数从 10 万降到 2 千。

3.3 采样数量由谁决定

ECharts 自动按容器像素宽度采样(大约每 2px 保留 1 个点)——这就是"反正看不见,不如不画"的落地。所以同一条曲线,宽屏上点数多一点,窄屏上少一点,视觉始终最优。

3.4 工业场景的选择题

场景 推荐 原因
温度趋势总览 lttb 保形状
告警监测(看瞬时峰值) max 尖峰不能被平均掉
振动信号平滑分析 average 要的就是均值趋势

🎯 面试高频题:“曲线上的瞬时尖峰被采样抹掉了怎么办?”——答:按业务语义选采样算法,峰值敏感场景用 max 或降低采样率,而不是无脑 lttb。


四、large 模式:放弃单元素交互换性能

4.1 默认模式的代价

默认情况下,每个数据点都是一个"可交互元素":ECharts 为它建独立的图形对象、注册进命中列表——1 万个散点就是 1 万个对象 + 1 万次命中测试。这是"每个点都能 hover 出 tooltip"的代价。

4.2 large 的取舍

const option = {
  series: [{
    type: "scatter",
    data: hundredThousandPoints,
    large: true,             // ⭐ 开启大数据模式
    largeThreshold: 2000,    // 点数超过 2000 才自动启用(默认值)
    // large 模式下:
    // ✅ 渲染走批量路径(一次性把所有点喂给 canvas,不分元素)
    // ❌ 单点 hover tooltip 失效(无法区分点中了哪个点)
    // ✅ 仍支持 visualMap 整体映射
  }],
};

取舍表

能力 默认模式 large 模式
单点 hover tooltip
点的独立动画
渲染速度(10 万点) 慢(逐元素) 快(批处理)
命中检测 O(n) 遍历 跳过

💡 折线图的 largeseries.large 对折线同样有效,优化的是**数据点标记(symbol)**的绘制。纯线条 + lttb 已经够快,symbol: ‘none’ 也是必开项(10 万个圆点标记是渲染杀手)。


五、progressive:渐进渲染不卡主线程

5.1 问题:首次渲染的白屏

10 万点的视觉编码(逐点生成图形属性)可能耗时 2~3 秒——期间主线程被占满,页面整个卡死(滚动、点击全部无响应)。

5.2 解法:分帧渲染

const option = {
  series: [{
    type: "scatter",
    data: hundredThousandPoints,
    progressive: 5000,             // 每帧渲染 5000 个元素
    progressiveThreshold: 3000,    // 元素超过 3000 才启用渐进
    progressiveChunkMode: "modulo",// 分块策略(ECharts 5.3+)
  }],
};

效果:元素分批画出来(先画 5000,下一帧再 5000…),每帧只占用几毫秒,页面保持响应,用户看到"逐渐成形"的动画过程——把一次性的 3 秒卡顿摊薄成 60 帧的轻负载。

5.3 适用与不适用

  • 散点图 / 大规模图形元素:渐进收益巨大
  • 动画本身:渐进是渲染策略不是动画,与 animationDuration 是两回事
  • 小数据量progressiveThreshold 以下不会启用,无副作用

六、appendData:增量追加

6.1 场景

实时监控流:每秒追加 1 个新点,图表维护最近 N 小时窗口。错误做法是每秒 setOption({ series: [{ data: 全量数组 }] })——数据越攒越多,每次都全量重算。

6.2 正确姿势

/**
 * 增量追加新数据点(不重发全量)
 * 注意:appendData 只能追加,不能删除——配合"定长窗口"需要定期全量重置
 */
chart.appendData({
  seriesIndex: 0,
  data: [[Date.now(), 84.2]],   // 追加的点
});

/**
 * 定长滚动窗口的完整策略:
 * - 高频(每秒):appendData 增量追加
 * - 低频(每 5 分钟):数据超过窗口上限时,setOption 全量替换裁剪后的数组
 */
const MAX_POINTS = 3600;   // 1 小时 @ 1s/点

setInterval(() => {
  const point = [Date.now(), readSensor()];
  if (buffer.length < MAX_POINTS) {
    chart.appendData({ seriesIndex: 0, data: [point] });
    buffer.push(point);
  } else {
    // 窗口满了:裁剪掉最旧的一小时段,全量替换
    buffer.shift();
    buffer.push(point);
    chart.setOption({ series: [{ data: [...buffer] }] });
  }
}, 1000);

⚠️ appendData 的限制:只对部分系列(line/scatter)有效、追加的数据不能改变维度结构。第 9 周 WebSocket 实时流会大量用到这个模式,今天先记住"高频增量 + 低频全量"的口诀。


七、渲染器与工程细节

7.1 canvas vs svg 渲染器

维度 canvas(默认) svg
大数据量(>1 万元素) ✅ 强 ❌ 弱(DOM 节点爆炸)
交互元素(hover 每个图形) 依赖内部命中检测 ✅ 原生 DOM 事件
导出矢量图 ❌ 位图 ✅ 可导 SVG
移动端小图 一般 ✅ 更清晰

工业大屏结论:一律 canvas——大数据量 + 高频更新是主场景。

7.2 工程细节清单

// 1. 组件卸载必须 dispose(单页应用内存泄漏头号来源)
function onUnmount(chart: echarts.ECharts, timer: number): void {
  clearInterval(timer);
  chart.dispose();          // 释放实例、解绑事件、清 canvas
}

// 2. 隐藏的 Tab/弹窗里的图表:暂停更新(省 CPU)
function onTabHidden(chart: echarts.ECharts): void {
  chart.setOption({ animation: false });   // 可选:关动画
  // 更彻底:clearInterval 停数据流,显示时恢复
}

// 3. resize 防抖(Day 52 讲过,高频 resize 是隐性能杀手)

// 4. 关闭不需要的默认能力
const option = {
  animation: false,                  // 实时流场景:动画反而干扰
  series: [{ symbol: "none" }],      // 折线不画点标记
  tooltip: { show: true },           // large 模式下 axis 触发仍可用
  hoverLayerThreshold: 1,            // 老版本优化项,5.x 已默认合理
};

八、压测实战:完整实验报告

8.1 实验设计

目标:10 万点折线图,从基线到优化的帧率与首次渲染耗时对比。

数据生成(仅开发环境压测用):

/**
 * 生成 10 万个模拟传感器点:正弦 + 噪声 + 随机尖峰
 */
function genSensorData(n: number): [number, number][] {
  const data: [number, number][] = [];
  const t0 = Date.now() - n * 1000;
  for (let i = 0; i < n; i++) {
    const t = t0 + i * 1000;
    const base = 80 + 10 * Math.sin(i / 3600);
    const noise = (Math.random() - 0.5) * 4;
    const spike = Math.random() < 0.0002 ? 25 : 0;   // 万分之二概率的尖峰
    data.push([t, Number((base + noise + spike).toFixed(2))]);
  }
  return data;
}

8.2 逐级优化与测量结果

测量方法:Performance 面板录制;帧率看拖拽/tooltip 移动 10 秒的 Frames 轨道;首次渲染看 setOption 到首帧绘制的 Long Task。

轮次 配置 首次渲染 交互帧率 结论
0 基线 默认配置 2.8s 卡死 18fps 不可用
1 sampling: 'lttb' + symbol: 'none' 320ms 55fps 采样见效
2 + large: true 290ms 58fps 散点场景收益更大,折线小幅提升
3 + progressive: 5000 首帧 40ms,1.2s 渐进完成 58fps 白屏消除,页面全程可交互
4 + animation: false 首帧 35ms 60fps 实时场景关动画
5 轮询追加 appendData 每秒 1 点 - 60fps 增量无感更新

📌 把这张表填上你自己的实测数据放进博客——"我做过 10 万点优化"和"我有压测数据"在面试里是两个物种。

8.3 优化配置终稿

/**
 * 大数据量折线的生产级配置模板
 */
const bigLineOption = {
  animation: false,
  tooltip: { trigger: "axis" },      // large 下 item 触发失效,axis 仍可用
  series: [{
    type: "line",
    data: genSensorData(100000),
    sampling: "lttb",
    large: true,
    symbol: "none",
    progressive: 5000,
    progressiveThreshold: 3000,
    lineStyle: { width: 1 },         // 线宽 2→1:10 万段的绘制量减半
  }],
};

九、常见坑点

坑 1:sampling 丢了峰值被告警漏报

average 采样把瞬时尖峰抹平,值班人员没看到告警。修复:峰值敏感用 sampling: 'max',或叠加一条 max 采样的辅助线。

坑 2:large 模式下 tooltip “失灵”

不是 bug,是设计取舍。修复:改用 tooltip.trigger: 'axis'(十字线 + 最近点,不依赖单元素命中)——你 Day 51 手写的就是这套,天然适合大数据。

坑 3:progressive 导致 “动画没播完”

渐进渲染的"逐渐成形"被误认为入场动画,和 animationDuration 叠加后观感混乱。修复:大数据量场景统一 animation: false,只留渐进效果。

坑 4:appendData 后轴范围不更新

追加的点超出当前 y 轴范围却看不见。原因:appendData 不触发轴重算。修复:定期全量 setOption(配合 6.2 的定长窗口策略),或手动设 yAxis.max

坑 5:单页应用切走回来图表消失/重叠

组件卸载没 dispose + 重新 init,或多个实例绑了同一容器。修复:生命周期钩子里严格 dispose,init 前 echarts.getInstanceByDom 查重。


十、自测挑战

T1 · 复现压测报告(40 分钟)

用 8.1 的数据生成器跑自己的 10 万点实验,把 8.2 的表格填上你的实测数据。今天核心作业,博客素材直接产出

T2 · 采样算法对比(20 分钟)

同一份带尖峰的数据分别用 lttb / average / max 渲染成三张图并排截图,标注尖峰保留情况。

T3 · 定长滚动窗口(30 分钟)

实现 6.2 的完整策略:setInterval 每秒 appendData,窗口 60 点,满后每 10 秒全量裁剪替换。观察内存(Performance Monitor 的 JS Heap)是否稳定。

T4 · 散点图三件套(20 分钟)

生成 5 万个散点(large + progressive + visualMap 分段色),对比默认配置与三件套配置的首帧时间和拖动流畅度。


十一、总结

今天的武器库按瓶颈对号入座:

症状 武器 代价
光栅化慢(拖动掉帧) sampling: 'lttb' 极端峰值可能失真
视觉编码慢(元素多) large: true 单点交互失效
首帧卡死主线程 progressive 无(摊薄而已)
流式数据全量重算 appendData 只能追加不能删
实时场景动画干扰 animation: false 无动画

明天回到"好看"的主线:工业大屏的仪表盘、雷达图、拓扑图与主题定制——性能是底座,视觉是工业大屏的说服力。

评论