数据条数过多时,加载缓慢,从获取到数据、处理数据、绘制图表渲染出来;
按照条件把这些大数据分为几个图表,同时绘制也是会存在卡顿。
就这几个过程,但是中间经过的点太多了,累积起来,都是时间、性能的考验和消耗。
AI其实把基本的性能优化以后,其他都靠时间日志对每一个步骤进行录制,再进行分析修改。
排查的时候就发现其实Echarts本身会有一些对于性能上的支持,虽然说有些时候会牺牲一些优美的样式。
我的项目里用了各式各样的symbol,这个开销可太大了,数据一多,使用symbol的时间比起不适用的时间成倍增长。
而且为了美观,tooltip里的图例直接使用了svg绘制,开销更大。
把更改的点进行了记录,也是一个警戒,后续再有类似的图表和数据量也可以直接就开始从部分点去约束。
一、数据层:减少进入图表的数据量
| 手段 | 说明 | 建议阈值 |
|---|---|---|
| 服务端降采样 | 接口返回前按时间窗口聚合,只返回前端需要的粒度 | 单条 series > 5000 点时 |
| 前端降采样算法 | LTTB 保留视觉特征(波峰波谷),优于等间隔采样 | 单条 > 1000 点时 |
| 等间隔采样 | 最简单,按固定步长取点,适合平缓曲线 | 精度要求不高时 |
| 按需加载 | 初始只加载概览数据,缩放/拖拽时再请求细节 | 时间跨度大时 |
LTTB 核心思路:将数据分桶,每桶选取与前一点和下一桶平均点构成最大三角形面积的点,比等间隔采样更好保留曲线形态。
二、ECharts 配置层:关闭不必要的功能
typescript
{
animation: false, // 关闭入场动画
hoverAnimation: false, // 关闭 hover 放大
silent: false, // 需要交互时不用 silent:true
sampling: 'lttb', // 启用内置采样(作为兜底)
progressive: true, // 渐进渲染
progressiveThreshold: 2000, // 超过此值启用
}
原则:每一项功能(动画、阴影、渐变、hover 效果)都有渲染成本,大数据量下能关则关。
三、Series 级配置
typescript
{
large: true, // 大数据模式,简化路径
largeThreshold: 500, // 超过此值启用 large
showSymbol: false, // 关闭标记点(最大性能提升点)
showAllSymbol: false, // 即使开 symbol 也只显示首尾
symbol: 'none', // 数据点多时直接设 none
symbolSize: 4, // 必须显示时用小尺寸
hoverAnimation: false,
emphasis: { disabled: true }, // 关闭高亮
lineStyle: { width: 1 }, // 线宽收窄
smooth: false, // 关闭平滑曲线
step: false, // 关闭阶梯
}
关键 :showSymbol 是性能影响最大的单项配置。标记点在每个数据点渲染一个 SVG 元素,数据量大时开销是纯线段的 5-10 倍。
四、Symbol 专项优化策略
是否需要显示标记点?
│
├─ 不需要 → showSymbol: false(最优)
│
└─ 需要 → 分级处理
│
├─ 数据点 > 80 → symbol: 'none'(自动隐藏)
│
├─ 数据点 > 500 → showAllSymbol: false + large: true
│
└─ 数据点 < 80 → 正常显示,但关闭 hoverAnimation
如果必须显示 symbol,配合更激进的降采样(比无 symbol 时再砍 40% 数据点)。
五、setOption 调用优化
typescript
// notMerge 模式:跳过内部 diff,直接替换
chart.setOption(option, true)
// 而非默认的 merge 模式
chart.setOption(option) // 会做深层 diff,大数据量时很慢
适用场景 :全量数据更新、切换数据源、切换显示模式时用 notMerge=true;局部更新(如只改颜色)用默认 merge。
六、渐进式渲染
typescript
// series 数量多时(> 100),分批渲染
async function progressiveRender(chart, allSeries, batchSize = 50) {
for (let i = 0; i < allSeries.length; i += batchSize) {
const batch = allSeries.slice(i, i + batchSize)
if (i === 0) {
chart.setOption({ series: batch })
} else {
chart.appendData({ seriesIndex: i }, batch)
}
await new Promise(r => setTimeout(r, 0)) // 让出主线程
}
}
原理:首批数据快速呈现给用户,后续批次在事件循环空闲时补充,避免长时间白屏。
七、交互优化
| 手段 | 实现 | 效果 |
|---|---|---|
| 数据处理防抖 | setTimeout(fn, 80) 合并连续调用 |
避免短时间内多次重渲染 |
| 数据请求节流 | _.throttle(fn, 3000) |
限制接口请求频率 |
| 事件按需绑定 | 重新渲染前先 chart.off() 解绑,渲染后重新 on() |
避免事件监听器堆积 |
| dataZoom 防抖 | dataZoom 事件触发后防抖再请求细节数据 | 拖拽过程中不频繁请求 |
| legend 交互优化 | legend 切换时用 merge 而非 notMerge |
只更新可见性,不重算全部 |
八、实例生命周期管理
typescript
onMounted(() => {
chart = echarts.init(el)
// resize 监听
resizeObserver = new ResizeObserver(() => chart.resize())
resizeObserver.observe(el)
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
chart?.dispose() // 必须释放,否则内存泄漏
chart = null
})
易遗漏点 :ResizeObserver 和 window.resize 监听器必须清理,否则组件卸载后仍触发 chart.resize() 报错。
九、内存与对象管理
| 问题 | 方案 |
|---|---|
| series 数组反复创建 | 复用对象,用 Object.assign 更新而非每次 {...s} |
| tooltip formatter 闭包引用旧数据 | 用函数返回最新数据,而非捕获变量 |
| 颜色计算结果缓存 | 相同参数的配色结果缓存,切换模式时复用 |
| 大数组 GC 压力 | 降采样时预分配 new Array(threshold) 而非 push |
十、多图表场景优化
分离模式(多个独立图表)
│
├─ 按可见区域懒渲染:只渲染视口内的图表,滚动时再渲染
│
├─ 共享配置:axis/grid/tooltip 配置抽取为公共对象
│
├─ 并行 batch:多个图表同时处理第一批,而非串行
│
└─ 独立策略:每个图表根据自身数据量独立决定降采样力度
十一、优化决策流程图
数据量评估
│
├─ 单条 < 500 点 → 默认配置即可
│
├─ 单条 500~2000 点 → animation:false + sampling:'lttb'
│
├─ 单条 > 2000 点 → 前端 LTTB 降采样到 500 点 + large:true
│
├─ series 数 > 50 → progressive 渐进渲染 + showSymbol:false
│
├─ series 数 > 200 → 激进降采样(48点/条) + appendData 分批
│
└─ 总点数 > 100000 → 服务端降采样 + 虚拟滚动 + Web Worker
十二、性能调试要点
- 不要用 console.log 打印大数组------会卡死 DevTools
- 用
performance.now()包裹关键阶段------定位是降采样慢还是渲染慢 - Chrome Performance 面板看 Long Task------超过 50ms 的任务会阻塞交互
- 调试日志必须彻底删除 ------即使
if(false)分支也会影响 V8 优化 - 对比有/无 symbol 的渲染耗时------通常差 3-5 倍