引言
ECharts 是国内业务图表的事实标准,但多数项目只用到它 20% 的能力:手写 series.data、在 useEffect 里反复 setOption、从不 dispose、全量引入 900KB 的包。这些做法在小项目里不出问题,一旦图表数量上到几十个、数据量上到十万级、或者需要服务端渲染,就会集中爆发。
真正的工程难点有四个。生命周期:实例不销毁导致内存泄漏与事件重复绑定,在 SPA 路由切换时尤其明显。数据流:直接操作 series.data 会让数据与视图耦合,改一个维度要动多处代码,dataset + transform 才是正确姿势。渲染性能:十万点散点用默认配置会直接卡死主线程,需要 large、progressive、sampling 三类开关配合。包体积:全量引入 900KB 未压缩,按需引入能压到 300KB 以内。
本文以 ECharts 5.5 为基准,按「架构 → 数据 → 交互 → 性能 → 集成」的顺序展开,每节都给可运行的配置片段。文中涉及的 API 均以官方文档为准,包括 echarts/core、echarts.use、dataset.transform、renderToSVGString 等。
目录
- 渲染模式与架构概览
- 实例生命周期与内存管理
- dataset 与数据变换
- 坐标系与多图布局
- 交互系统与事件处理
- 按需引入与包体积优化
- 主题定制与视觉规范统一
- 大数据量渲染优化
- 图表联动与跨组件通信
- 服务端渲染与静态导出
- 与 React/Vue 集成模式
- 版本升级与迁移陷阱
1. 渲染模式与架构概览
ECharts 5 的渲染层抽象成了两个后端:Canvas 与 SVG。默认是 Canvas,适合大数据量与复杂交互;SVG 适合需要 DOM 可访问性、需要被 CSS 影响、或图表元素数量少的场景。
选择依据很明确:元素数量超过 1000 选 Canvas,低于 1000 且需要 SEO/无障碍/矢量导出选 SVG。Canvas 在大元素量下性能优势明显,但它在无障碍上几乎是空白——屏幕阅读器读不到任何内容,只能靠外挂的 aria-label 与数据表。SVG 的每个图形都是 DOM 节点,可以被读屏软件遍历,也支持 CSS 变量改色。
import * as echarts from 'echarts';
const chart = echarts.init(dom, null, { renderer: 'canvas' }); // 默认
const svgChart = echarts.init(dom, null, { renderer: 'svg' }); // 无障碍/矢量导出
架构上,ECharts 分为 echarts/core(核心)、echarts/charts(图表类型)、echarts/components(组件)、echarts/renderers(渲染器)四层。全量引入把四层全打包,按需引入只取用到的部分——这是后面第 6 节的基础。
2. 实例生命周期与内存管理
每个 echarts.init 都会在 DOM 上挂一个实例,并注册 window 的 resize 监听。不调用 dispose 就不会释放,在 SPA 里连续切换路由会累积几十个僵尸实例,每个都持有数据引用与 canvas 上下文。
正确的生命周期是「挂载时 init、卸载时 dispose」,且 resize 监听要用 ResizeObserver 而不是 window 事件:
import * as echarts from 'echarts';
export function mountChart(dom, option) {
// 复用已存在的实例,避免重复 init
const chart = echarts.getInstanceByDom(dom) ?? echarts.init(dom);
chart.setOption(option, { notMerge: false });
// 用 ResizeObserver 响应容器尺寸变化,比 window.resize 更精确
const ro = new ResizeObserver(() => {
if (dom.clientWidth > 0 && dom.clientHeight > 0) chart.resize();
});
ro.observe(dom);
return () => { ro.disconnect(); chart.dispose(); };
}
三个易踩的细节:其一,容器必须在 init 前就有确定的宽高,否则会得到 0×0 的画布,需要显式传 { width, height };其二,容器被 display: none 隐藏时 resize 会得到 0,恢复显示后必须再调一次 resize();其三,setOption 的 notMerge 参数决定是合并还是替换,默认 false(合并)在切换图表类型时会残留旧配置,切换类型时应传 true。
3. dataset 与数据变换
dataset 是 ECharts 5 最重要的数据抽象。它把数据源与视觉映射解耦,支持三种数据形式:二维数组、对象数组、以及带 dimensions 描述的对象。配合 encode 声明哪个维度映射到哪个通道,配置不再依赖 series.data 的隐式顺序。
const option = {
dataset: {
source: [
{ date: '2026-01-01', product: 'A', sales: 820, profit: 120 },
{ date: '2026-01-01', product: 'B', sales: 640, profit: 95 },
],
},
xAxis: { type: 'category' },
yAxis: { type: 'value' },
series: [{
type: 'bar',
// encode 显式声明维度映射,源数据列顺序变化不影响
encode: { x: 'date', y: 'sales', itemName: 'product', tooltip: ['sales', 'profit'] },
}],
};
transform 提供了内置的数据变换能力,能在不改后端查询的情况下做过滤、排序、聚合:
dataset: [
{ source: rawRows },
{ transform: { type: 'filter', config: { dimension: 'profit', gt: 0 } } },
{ transform: { type: 'sort', config: { dimension: 'sales', order: 'desc' } } },
// 自定义变换需注册,如 ecStat:regression
],
数据变换应该在前端做的边界是:行数不超过几万、变换不涉及跨表关联。真正的聚合、关联、去重应放在 OLAP 侧完成,前端只做轻量的展示层调整。这与数仓集成的分工一致,可对照 可视化与 OLAP 数仓集成 。
4. 坐标系与多图布局
ECharts 的坐标系是一个可插拔的抽象:grid(直角坐标系)、polar(极坐标)、geo(地理)、calendar、singleAxis、radar、parallel。同一个图表类型可以挂到不同坐标系上——line 系列既能画在 grid 上,也能画在 polar 上形成雷达式的螺旋。
多图布局用多个 grid 配合 xAxisIndex/yAxisIndex 实现,这在仪表盘里非常常见:
const option = {
grid: [
{ left: '5%', right: '55%', top: '10%', height: '35%' }, // 左上
{ left: '55%', right: '5%', top: '10%', height: '35%' }, // 右上
{ left: '5%', right: '5%', top: '55%', height: '35%' }, // 下方通栏
],
xAxis: [{ gridIndex: 0, type: 'category' }, { gridIndex: 1, type: 'category' },
{ gridIndex: 2, type: 'time' }],
yAxis: [{ gridIndex: 0, type: 'value' }, { gridIndex: 1, type: 'value' },
{ gridIndex: 2, type: 'value' }],
series: [
{ type: 'bar', xAxisIndex: 0, yAxisIndex: 0, data: [120, 200, 150] },
{ type: 'pie', center: ['78%', '28%'], radius: '28%', data: pieData },
{ type: 'line', xAxisIndex: 2, yAxisIndex: 2, data: trend },
],
};
grid 的定位支持百分比与像素混用,但混用时要注意容器尺寸变化后百分比会重算而像素不会。推荐全部用百分比,或者在 resize 回调里重算。多坐标系之间的对齐(多个 grid 的 X 轴刻度对齐)需要手动设置相同的 left/right,ECharts 不会自动对齐。
5. 交互系统与事件处理
ECharts 的交互分三层:内置交互(tooltip、dataZoom、legend 点击)、组件交互(brush 选择、toolbox)、以及用户事件(click、mouseover、legendselectchanged 等)。
事件绑定要在实例上做,而不是在 DOM 上——只有实例事件才能拿到数据项信息:
// params 包含 componentType / seriesIndex / dataIndex / value / name
chart.on('click', (params) => {
if (params.componentType === 'series' && params.seriesType === 'bar') {
drillDown(params.name, params.dataIndex);
}
});
// 图例开关事件:params.selected 形如 { '华东': true, '华南': false }
chart.on('legendselectchanged', (params) => syncOtherCharts(params.selected));
// 监听 dataZoom,用于服务端拉取更细粒度数据
chart.on('datazoom', () => {
const { start, end } = chart.getOption().dataZoom[0];
scheduleFetch(start, end);
});
dataZoom 的 start/end 是百分比(0 到 100),要转成时间范围需要结合轴的范围计算。如果开启 filterMode: 'filter',缩放会真实过滤数据;若用 'none',只改变视窗不改数据,性能更好但 Y 轴不会自适应。
tooltip 的性能陷阱:trigger: 'axis' 配合大数据量时,每次移动鼠标都要重新计算所有系列的当前值。当系列超过 10 个或数据点超过 1 万时,应设置 tooltip.transitionDuration: 0 并考虑关闭 axisPointer 的动画。
6. 按需引入与包体积优化
全量引入的 import * as echarts from 'echarts' 会打包约 900KB(未压缩)。按需引入通过 echarts/core 加显式注册,能压到 300KB 以内,配合 tree-shaking 效果更好:
// 只引入用到的模块
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, LegendComponent,
TitleComponent, DataZoomComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent,
LegendComponent, TitleComponent, DataZoomComponent, CanvasRenderer]);
export default echarts;
| 引入方式 | 未压缩体积 | gzip 后 | 适用场景 |
|---|---|---|---|
全量 echarts | ~900KB | ~330KB | 快速原型 |
按需 echarts/core | ~350KB | ~130KB | 生产环境 |
| 按需 + 仅 bar/line | ~250KB | ~95KB | 图表类型固定 |
| SSR 导出 | 0(服务端) | 0 | 静态图/邮件报表 |
还有一个进阶手段是自定义构建:用 echarts/build/build.js 从源码构建只含所需模块的包,能进一步去掉未使用的语言包与主题。这对首屏性能敏感的产品值得投入,但对多数业务项目,按需引入已经足够。
7. 主题定制与视觉规范统一
主题决定了图表的视觉一致性。ECharts 支持注册全局主题、按实例指定主题、以及用 setOption 局部覆盖三层。推荐的做法是注册一个品牌主题 + 用设计令牌生成色板:
const brandTheme = {
color: ['#2f6fed', '#22a06b', '#e8a33d', '#d64545', '#7a5af8', '#00a3b4'],
backgroundColor: 'transparent',
textStyle: { fontFamily: 'Inter, "PingFang SC", sans-serif', fontSize: 12 },
title: { textStyle: { color: '#1a1a1a', fontSize: 14, fontWeight: 600 } },
categoryAxis: { axisLine: { lineStyle: { color: '#d9d9d9' } }, axisTick: { show: false } },
// 网格线极浅,接近背景色,避免稀释数据墨水比
valueAxis: { axisLine: { show: false }, splitLine: { lineStyle: { color: '#f0f0f0' } } },
tooltip: { backgroundColor: 'rgba(255,255,255,0.96)', borderColor: '#e0e0e0' },
};
echarts.registerTheme('brand', brandTheme);
const chart = echarts.init(dom, 'brand');
色板的选择要遵循第 2 篇的感知原则——分类色板任意两色可区分、明度接近。用 OKLCH 生成能保证这一点。不要在每个图表里手写 itemStyle.color,那样主题就形同虚设,改一次品牌色要动几十处。
8. 大数据量渲染优化
十万级数据点是 ECharts 的性能分水岭。默认配置下,散点图超过约 2 万点、折线图超过约 5 万点就会出现明显卡顿。四类优化手段按收益排序:
第一,large 模式。开启后 ECharts 用优化的绘制路径跳过大量样式计算,只支持有限的样式配置:
series: [{
type: 'scatter',
large: true,
largeThreshold: 2000, // 超过此数量启用 large 模式
symbolSize: 3,
data: bigArray, // 10 万点
}]
第二,渐进渲染 progressive。把一次渲染拆成多帧,避免长任务阻塞主线程:
series: [{
type: 'scatter',
progressive: 4000, // 每帧渲染 4000 点
progressiveThreshold: 3000, // 超过 3000 点启用渐进
data: hugeArray,
}]
第三,降采样 sampling。折线图专用,把点数降到屏幕像素量级:
series: [{
type: 'line',
sampling: 'lttb', // Largest-Triangle-Three-Buckets,保留极值
showSymbol: false, // 关掉符号,避免上万次绘制
data: timeSeries,
}]
第四,减少交互开销。关闭 animation、把 tooltip.trigger 改成 'item'、关闭 emphasis 的缩放效果。
| 数据量 | 散点图 | 折线图 | 推荐配置 |
|---|---|---|---|
| < 2k | 默认 | 默认 | 无 |
| 2k ~ 10k | large: true | sampling: 'lttb' | 关 symbol |
| 10k ~ 100k | large + progressive | sampling + progressive | 关动画 |
| > 100k | 分箱聚合 | 服务端降采样 | 换 heatmap |
超过十万点时,正确的做法不是继续压榨前端,而是在服务端或 OLAP 侧做聚合(按像素桶聚合、或按时间粒度降采样),前端只渲染聚合后的几千个点。这与大屏渲染优化是同一套思路,细节见 大屏渲染性能与优化 。
9. 图表联动与跨组件通信
仪表盘里的图表需要联动:点某个区域的柱子,其他图表跟着过滤。ECharts 原生只提供 connect 做同源实例的 dataZoom/legend 联动,业务级联动需要自己实现一个轻量的事件总线。
// 用原生 connect 做视图联动(dataZoom / tooltip 同步)
echarts.connect([chartA, chartB, chartC]);
// 业务联动:点击 A 的柱子,高亮 B 的对应数据项
chartA.on('click', (params) => {
chartB.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: params.dataIndex });
chartA.dispatchAction({ type: 'selectRegion', name: params.name });
});
// 注册自定义 action,让外部组件可订阅
echarts.registerAction({ type: 'selectRegion', event: 'regionSelected' }, () => {});
dispatchAction 是 ECharts 对外暴露的命令接口,比直接 setOption 更轻量(不重建视图)。常见 action 类型有 highlight、downplay、select、dataZoom、showTip。注册自定义 action 后可通过 chart.on('regionSelected', cb) 让外部组件订阅,形成单向数据流。
10. 服务端渲染与静态导出
ECharts 5.3 起支持 SSR:在 Node 端直接生成 SVG 字符串,不需要浏览器环境。这对邮件报表、PDF 导出、静态站点非常有用。
import * as echarts from 'echarts';
export function renderChartToSVG(option, width = 800, height = 400) {
// 传 null 作为容器,ssr: true 表示不挂载到 DOM
const chart = echarts.init(null, null, { renderer: 'svg', ssr: true, width, height });
chart.setOption(option);
const svg = chart.renderToSVGString();
chart.dispose(); // SSR 实例也要释放
return svg;
}
注意 SSR 模式的两个限制:动画被忽略(静态输出无所谓),交互组件不生效(tooltip、dataZoom 在静态图里没有意义,应在导出前用 option 关掉)。导出 PNG 则仍需浏览器环境(canvas.toDataURL),可以用无头浏览器在 CI 里批量生成。如果站点本身是 SSG 的,把图表预渲染成 SVG 内联进 HTML 能显著改善首屏——用户看到图之前不需要加载 300KB 的 JS,可参考 SSR 与 SSG 架构选型
。
11. 与 React/Vue 集成模式
框架集成的核心问题是何时 setOption、何时 dispose、如何避免重复渲染。三种模式:
模式一:封装成组件,用 ref 持有实例。这是最通用的做法:
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
export function Chart({ option, height = 320 }: { option: echarts.EChartsOption; height?: number }) {
const ref = useRef<HTMLDivElement>(null);
const chartRef = useRef<echarts.ECharts>();
useEffect(() => {
if (!ref.current) return;
const chart = echarts.init(ref.current);
chartRef.current = chart;
const ro = new ResizeObserver(() => chart.resize());
ro.observe(ref.current);
return () => { ro.disconnect(); chart.dispose(); };
}, []);
// option 变化时只更新,不重建实例
useEffect(() => { chartRef.current?.setOption(option, { notMerge: true }); }, [option]);
return <div ref={ref} style={{ width: '100%', height }} />;
}
模式二:用官方 wrapper(echarts-for-react),省事但版本耦合紧,出问题时排查链更长。
模式三:Vue 3 的 shallowRef。Vue 的响应式代理会深度代理 ECharts 实例,导致性能问题甚至报错,必须用 shallowRef 持有实例:
import { shallowRef, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
const container = shallowRef<HTMLDivElement>();
const chart = shallowRef<echarts.ECharts>(); // 不能用 ref()
onMounted(() => {
chart.value = echarts.init(container.value!);
});
onBeforeUnmount(() => chart.value?.dispose());
React 的 StrictMode 在开发环境会双调用 effect,导致 init 两次。上面的 cleanup 里调 dispose 能正确处理,但要确保 getInstanceByDom 的复用逻辑到位,否则会看到「已有实例」的警告。
12. 版本升级与迁移陷阱
从 4.x 升到 5.x 的破坏性变更集中在三处。其一,legend 默认行为:5.x 里 legend.selected 的初始状态与 4.x 不同,依赖默认全选逻辑的代码需要显式设置。其二,tooltip.formatter 回调参数:5.x 传的是数组而非单个对象。其三,color 默认色板:5.x 换成了新的 8 色板,视觉回归测试会大面积失败。
// 5.x 的 tooltip formatter 拿到的是数组
tooltip: {
trigger: 'axis',
formatter: (params) => {
const list = Array.isArray(params) ? params : [params];
const head = `<div style="font-weight:600">${list[0].axisValue}</div>`;
const body = list.map((p) => `${p.marker}${p.seriesName}: ${p.value.toLocaleString()}`)
.join('<br/>');
return head + body;
},
}
另一个高频问题是 series.id 缺失导致更新时系列错位。当系列数量动态变化时,ECharts 按数组下标做 diff,若没有稳定的 id,颜色与图例状态会串到别的系列上。动态系列必须设置 id。
权衡取舍
| 决策点 | 选项 A | 选项 B | 何时选 A | 何时选 B |
|---|---|---|---|---|
| 渲染器 | Canvas | SVG | 元素 >1000、大数据量 | 需要无障碍/矢量导出 |
| 数据源 | series.data 直写 | dataset + encode | 单一静态图 | 多图共享数据 |
| 变换位置 | 前端 transform | OLAP 侧聚合 | 行数 <1 万 | 需要关联/去重 |
| 引入方式 | 全量 | 按需 use() | 原型验证 | 生产环境 |
| 更新方式 | setOption 全量 | dispatchAction | 数据结构变化 | 仅状态变化 |
| 大数据 | large/progressive | 服务端聚合 | 2 万~10 万点 | >10 万点 |
常见坑清单
- 不调用 dispose——SPA 路由切换累积僵尸实例,内存持续增长;卸载时务必 dispose 并断开 ResizeObserver。
- 容器宽高为 0 时 init——得到 0×0 画布,图表不可见;确保 init 前容器已有布局尺寸或显式传 width/height。
- display:none 时 resize——宽高读到 0 导致图表塌缩;恢复显示后补调一次 resize。
- 动态系列不设 id——按下标 diff 导致颜色与图例状态串位;每个系列给稳定 id。
- 切换图表类型用合并模式——旧 series 残留,出现幽灵图形;切换类型时
notMerge: true。 - Vue 用 ref 持有实例——响应式代理深代理 ECharts 实例,性能骤降甚至报错;改用 shallowRef。
- 十万点用默认配置——主线程长任务卡死页面;开 large + progressive 或改服务端聚合。
- 全量 import echarts——包体积 900KB;按需 use() 可压到 130KB gzip。
- resize 监听用 window——容器尺寸变化(如侧边栏收起)不触发;改用 ResizeObserver。
- tooltip formatter 当对象用——5.x 传数组,单系列时侥幸可用,多系列时报错;统一按数组处理。
小结
ECharts 的工程化要点可以归纳成四组动作:生命周期上坚持「init 复用 + dispose 释放 + ResizeObserver 监听」;数据上统一走 dataset + encode,把重聚合下沉到 OLAP;性能上按数据量分级启用 large、progressive、sampling;集成上按需引入并用 shallowRef(Vue)或 ref + effect(React)持有实例。
版本升级的风险主要来自隐式的默认行为变化——色板、图例初始态、formatter 参数形态。做视觉回归测试能在升级时快速定位这些差异,比逐个图表肉眼比对可靠得多。
如果图表类型需要超出 ECharts 内置能力的定制,可以对比 AntV G2 声明式图表体系 的图形语法抽象,或者直接用 D3.js 与图形语法 从底层构建;多图组合与布局设计见 仪表盘与数据大屏设计 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。