1. 折线图这件事ECharts 在 Vue 项目里到底扮演什么角色但凡做过后台管理、监控大屏、数据分析面板的人大概都有过这种经历产品上午提了一句“把趋势数据展示一下”下午就要看到页面上有一条光滑的曲线在跑。绝大多数人的第一反应是打开 ECharts 官网找到折线图示例把那段 option 复制下来塞进 Vue 组件的 mounted 里。看起来五分钟就能收工真正落地的时候却几乎必然在几个地方卡住——横轴标签挤成一团黑线、浏览器窗口一缩放图表就糊或者变形、接口返回新数据了线条纹丝不动、路由切走之后内存里还躺着十几个没销毁的图表实例。这篇文章想聊的就是这些“复制示例之后”的事。核心围绕Vue ECharts 绘制折线图这条主线从环境准备、最小可运行版本到轴刻度控制、双 Y 轴混合图、实时数据刷新、自适应与销毁、按需引入瘦身把一条折线从“能画出来”推进到“能上线、能维护、不惹事”。适合刚开始接触 Vue 数据可视化的新手也适合已经在项目里用了半年 ECharts、但每次遇到 resize 和内存泄漏都要临时搜一圈的中级开发者。文中所有代码基于 Vue 3 的组合式 API 和 ECharts 5.xVue 2 的朋友把onMounted换成mounted、把shallowRef换成this.$refs就能平移思路完全一致。1.1 为什么折线图场景下 ECharts 依然是首选先解决一个前置问题为什么不用 Chart.js、不用 Highcharts甚至不用手写 Canvas。折线图本身逻辑不复杂但业务里的折线图从来不“纯粹”。它经常要叠一条柱状图表示订单量、叠一条虚线表示目标基线、叠一个面积填充表达区间还要支持拖拽缩放、鼠标悬浮十字准星、点击某个点下钻到明细。这种“一条线演化成一张复合图”的需求ECharts 的配置项体系覆盖得最完整。方案优势折线图场景下的短板ECharts配置项齐全、双 Y 轴/混合图/数据缩放开箱即用、中文文档与社区资料多包体积偏大需要按需引入控制Chart.jsAPI 简洁、体积小双轴、复杂 tooltip、大数据量降采样要自己补Highcharts交互细腻、文档规范商业授权限制团队内部引入要先过合规手写 Canvas / SVG完全可控坐标计算、坐标轴、tooltip 全要自己写投入产出比极低我自己在几个项目里都试过绕开 ECharts最后结论很一致只要图里出现第二根轴或者出现“柱状图叠加折线图”这种需求回 ECharts 是最省时间的路。ECharts 的社区资源密度也确实高遇到tooltip自动换行、x轴刻度显示不全这类具体问题搜一下基本都能找到现成方案。1.2 折线图在真实业务里的四类典型形态把 ECharts 折线图理解成“一条曲线”会限制你对它的使用。实际项目里我遇到的大致分四类每一类在配置上的侧重点完全不同。趋势类日活、周活、GMV 随时间变化。重点在x轴的时间粒度和刻度密度smooth要不要开areaStyle的渐变要不要加。这类图最怕横轴标签重叠也最怕数据点太多导致线条抖动。对比类今年和去年、A 渠道和 B 渠道。重点在legend的交互、多条线的颜色区分度、series数组的动态增删。数据系列可能不固定需要replaceMerge才能安全替换。监控类CPU 使用率、在线人数每隔几秒刷新一次。重点在实例复用、数据窗口滑动、animation关闭或缩短以及组件销毁时的dispose。这类图写不好半小时后浏览器就会卡成幻灯片。混合类折线表示转化率、柱状表示绝对量。重点在双yAxis、yAxisIndex映射、两个轴刻度对齐、图例分组。这类图最容易出现“柱子被压成一条线”的视觉事故本质是量纲差异没处理好。1.3 划清边界数据归 Vue渲染归 ECharts这是我认为最重要的一条心智模型理解透它能省掉后面一大半的诡异 bug。ECharts 是一个有自己内部状态、自己渲染循环、自己 DOM 结构的独立库它不关心 Vue 的响应式也不会自动跟着ref变化去重绘。Vue 负责的是“数据从哪来、什么时候变、变成什么”ECharts 负责的是“拿到这份数据之后怎么画、怎么动”。一旦这个边界模糊就会出现两类经典问题。第一类是“用 Vue 的思维去驱动图表”——把 option 写成一个computed指望它变了图表就自动变结果发现必须手动setOption。第二类是“用 ECharts 的思维去管理状态”——把图表实例挂到data或reactive里让它被 Proxy 层层包裹然后resize时报一堆莫名其妙的内部错误。2. 从零开始Vue 项目里接入 ECharts 的最小闭环真正动手之前先把环境这一步做干净。很多人卡在echarts.init返回空、图表不显示八成不是代码写错了而是容器高度或者初始化时机的问题。这一节把最小可运行版本完整走一遍。2.1 依赖安装与版本选择用 npm 或 pnpm 都行看你团队习惯。我偏向 pnpm装依赖快、磁盘占用小但这不是重点。# npm npm install echarts --save # pnpm pnpm add echarts # yarn yarn add echarts版本上建议锁在 ECharts 5.x 的最新小版本。5.x 相对 4.x 有几个折线图场景直接受益的变化axisLabel.hideOverlap可以自动隐藏重叠的横轴刻度标签sampling的采样策略更成熟renderer的 Canvas 和 SVG 切换更顺畅。如果你的项目还停在 4.xhideOverlap用不了得靠interval函数手动算。注意不要同时装echarts和echarts-for-react、vue-echarts这类封装库再各自引入一份 ECharts 内核打包体积会翻倍。要么用封装库要么自己封装选一条路走到底。至于要不要用vue-echarts这类现成组件我的看法是项目里只有一两张图用现成组件确实省事项目里有十几张图、且需要统一管理 resize 和主题自己封一层反而更清楚因为你能完全掌握setOption的时机和参数。2.2 组件骨架容器高度是第一个坑先看骨架再解释为什么这么写。template div classchart-wrap div refchartRef classchart-canvas/div /div /template script setup import { ref, shallowRef, onMounted, onBeforeUnmount } from vue import * as echarts from echarts const chartRef ref(null) const chartInstance shallowRef(null) onMounted(() { initChart() }) onBeforeUnmount(() { chartInstance.value?.dispose() chartInstance.value null }) function initChart() { if (!chartRef.value) return chartInstance.value echarts.init(chartRef.value) chartInstance.value.setOption(buildOption()) } /script style scoped .chart-wrap { width: 100%; height: 320px; } .chart-canvas { width: 100%; height: 100%; } /style这段代码里有三个细节值得单独说。第一个是shallowRef而不是ref。ECharts 实例对象内部结构极其复杂包含大量 DOM 引用、定时器、Canvas 上下文。如果用ref或reactiveVue 会对它做深层响应式代理ECharts 内部做instanceof判断、拿原生 DOM 引用时可能拿到代理对象轻则行为异常重则报错。用shallowRef只代理最外层引用问题直接消失。这是很多“图表在某些操作后突然失灵”的根因。第二个是高度。ECharts 的容器必须有明确的计算高度height: 100%的父级链上只要有一环是auto最终容器高度就是 0画布尺寸也就是 0页面上什么都不显示控制台也不报错。我见过最隐蔽的一次是父级用了 flex 但没给高度排查了四十分钟才发现。第三个是初始化时机。onMounted之后 DOM 已经挂载容器有尺寸这时候init最稳妥。如果你在setup顶层直接initDOM 还没渲染出来拿不到元素。2.3 第一张折线图完整可复制代码接着把 option 补全。下面这份是“能直接用”的版本包含标题、坐标轴名、图例、平滑曲线、面积渐变和 tooltip 格式化。function buildOption() { return { title: { text: 近 7 日订单趋势, left: 0, textStyle: { fontSize: 14, fontWeight: 600 } }, tooltip: { trigger: axis, axisPointer: { type: cross }, backgroundColor: rgba(255,255,255,0.96), borderColor: #e5e7eb, textStyle: { color: #333, fontSize: 12 }, valueFormatter: (v) ${v} 单 }, legend: { data: [线上, 线下], right: 0, top: 0, icon: roundRect, itemWidth: 12, itemHeight: 4 }, grid: { left: 8, right: 8, top: 48, bottom: 8, containLabel: true }, xAxis: { type: category, boundaryGap: false, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日], axisLine: { lineStyle: { color: #e5e7eb } }, axisTick: { show: false }, axisLabel: { color: #6b7280, fontSize: 12 } }, yAxis: { type: value, name: 订单量, nameTextStyle: { color: #9ca3af, padding: [0, 0, 0, -24] }, splitLine: { lineStyle: { type: dashed, color: #f0f1f3 } }, axisLabel: { color: #6b7280, fontSize: 12 } }, series: [ { name: 线上, type: line, smooth: true, showSymbol: false, symbolSize: 6, lineStyle: { width: 2 }, areaStyle: { opacity: 0.18, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #3b82f6 }, { offset: 1, color: rgba(59,130,246,0) } ]) }, data: [120, 200, 150, 80, 70, 110, 130] }, { name: 线下, type: line, smooth: true, showSymbol: false, lineStyle: { width: 2 }, data: [60, 90, 130, 100, 85, 95, 70] } ] } }几个参数顺手解释一下都是实打实调过的。grid.containLabel: true是必须的它会让 ECharts 自动把坐标轴标签的空间算进 grid 里否则y轴名称或者最后一位数字经常被裁掉。grid.left: 8配containLabel是常用组合看起来比默认的left: 10%整齐得多尤其是容器宽度不固定的时候。showSymbol: false在数据点超过二三十个的时候建议关掉圆点会把曲线糊成一条带珠子的绳子。axisPointer.type: cross打开十字准星鼠标在图上移动时能同时看到横纵两个方向的定位线比默认的竖线体验好一档。areaStyle里的LinearGradient是折线图提升质感的性价比最高的一个改动从线色渐变到透明视觉上比纯色填充轻很多。opacity: 0.18是我试出来的比较舒服的值低于 0.1 几乎看不见高于 0.3 会压住底下的网格线。3. 折线图的核心配置项逐条拆解最小版本跑起来之后真正的难点才开始。这一节挑几个最高频、最容易出问题的配置项把“为什么这么设”讲透。3.1 xAxis类目轴还是时间轴先想清楚这是折线的横轴第一次要做的决策。类目轴type: category把每个值当成等距的离散刻度不管实际间隔多久周一到周日就是七等份。时间轴type: time按真实时间戳计算位置两个数据点隔了 3 天和隔了 3 小时在图上占的宽度完全不同。判断标准很简单如果横轴是固定粒度的统计结果每日、每周、每月用类目轴data直接给格式化好的字符串。如果横轴是不定期的采集点比如设备每隔一段时间上报一次或者两个时间点之间的间隔本身有业务含义用时间轴。时间轴的数据结构有两种写法别写混// 写法一series.data 传 [时间, 值] 二元数组xAxis 不传 data xAxis: { type: time }, series: [{ type: line, data: [ [2024-05-01 08:00, 12], [2024-05-01 08:15, 19], [2024-05-01 09:40, 7] ] }] // 写法二xAxis.data 传时间数组series.data 只传值 xAxis: { type: time, data: [2024-05-01 08:00, 2024-05-01 08:15, 2024-05-01 09:40] }, series: [{ type: line, data: [12, 19, 7] }]我一般用写法一因为数据源通常就是[{time, value}]的结构直接map一下就行不用维护两个平行数组。更要紧的是两个数组一旦长度不一致ECharts 不会报错只会画出诡异的图排查起来很费时间。3.2 x 轴刻度挤成一坨三层解决思路「echarts 折线图 x 轴刻度」这个问题我估计每周都能在群里看到。现象是数据点一多横轴标签叠在一起变成一条黑带。解决手段按优先级排三层。第一层让 ECharts 自己处理。ECharts 5.x 在axisLabel上提供了hideOverlapxAxis: { type: category, axisLabel: { hideOverlap: true, // 自动隐藏会重叠的标签 interval: auto // 默认值让 ECharts 算步长 } }interval: auto是默认行为它会按容器宽度和数据量自动决定每隔几个显示一个标签。很多人以为标签挤在一起是因为没设interval其实默认就在起作用只是数据量太大时它算出来的步长仍然不够用。第二层手动控制步长和旋转。interval可以传数字表示“每隔 N-1 个跳过”传 0 表示全部显示。xAxis: { axisLabel: { interval: 0, // 强制全部显示慎用 rotate: 40, // 倾斜角度0~90 margin: 12, color: #6b7280 } }这里有个经验值rotate一旦超过 45 度人眼阅读斜排文字的效率会明显下降而且占用大量底部空间。所以在移动端窄容器里我更倾向用formatter把标签截短而不是靠旋转硬塞。第三层用formatter做压缩和换行。时间戳格式化成短格式长文本用换行符硬断。xAxis: { axisLabel: { formatter: (val) { // 2024-05-01 - 05/01 if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return val.slice(5).replace(-, /) // 中文长标签强制换行每 4 个字一行 return val.length 4 ? val.replace(/(.{4})/g, $1\n) : val } } }提醒formatter返回带\n的字符串在类目轴上是有效的但axisLabel.lineHeight得配合设置否则两行文字会贴在一起。另外interval: 0加rotate只在数据点少于 20 个的时候还能看再多就必须承认容器的物理宽度不够该考虑用dataZoom让用户自己拖。3.3 yAxis量纲、刻度与双轴混合图y轴最常见的两个问题一是刻度值全是整数但业务需要小数二是两个量纲差几十倍的数据放在一起小的那条被压成直线。先说不开双轴的情况。yAxis默认type: value会自动算min、max和splitNumber默认 5 段。如果数据都是整数ECharts 会自动把刻度取整这时候如果业务需要显示0.5这种刻度得手动干预yAxis: { type: value, min: 0, max: 100, interval: 20, // 强制每格 20配合 min/max 保证刻度整齐 splitNumber: 5, // 不设 interval 时生效 axisLabel: { formatter: (v) ${v}% } }min、max、interval三个一起设的时候splitNumber会被忽略。我踩过的坑是只设了min和max没设interval结果刻度变成0、12.5、25、37.5...这种别扭的数后来一律三个一起写死。再说双轴。核心就三步yAxis写成数组、series里用yAxisIndex指认、两个轴的splitNumber保持一致让网格线对齐。yAxis: [ { type: value, name: 转化率(%), splitNumber: 5, axisLabel: { formatter: {value}% }, splitLine: { lineStyle: { type: dashed, color: #f0f1f3 } } }, { type: value, name: 订单量(单), splitNumber: 5, axisLabel: { formatter: {value} }, splitLine: { show: false } // 第二个轴不画网格线避免视觉打架 } ], series: [ { name: 转化率, type: line, yAxisIndex: 0, smooth: true, data: [3.2, 4.1, 3.8, 5.2, 4.9, 5.6, 6.1] }, { name: 订单量, type: bar, yAxisIndex: 1, barWidth: 42%, itemStyle: { borderRadius: [4, 4, 0, 0] }, data: [120, 200, 150, 80, 70, 110, 130] } ]这里最关键的细节是第二个yAxis的splitLine.show: false。两个轴都画网格线图里会出现横线交错、密度翻倍的情况非常乱。惯例是只让左边的主轴画线。另外“柱状图叠加折线图”的层级也有讲究。ECharts 默认按series数组顺序绘制柱状在前、折线在后折线就不会被柱子挡住。如果你的折线被柱子盖住了把折线的z值调大比如z: 10即可不用改数组顺序。3.4 series平滑、符号、面积与堆叠series里能调的东西最多挑几个直接影响观感的。smooth建议开但别开成“过度平滑”。smooth: true默认平滑系数 0.5曲线会比较柔如果数据波动很剧烈平滑之后会掩盖真实拐点做监控图的时候我一般设smooth: 0.2或者干脆关掉。数据可视化的底线是别为了好看牺牲真实性。showSymbol和symbolSize联动。数据点少于 15 个的时候显示圆点有助于看清每个点超过 30 个就该关掉。折线交叠的地方还可以用symbol: emptyCircle让点中心留白视觉上更透气。areaStyle前面提过渐变这里补一点多条线同时开面积填充时要谨慎重叠区域的颜色会互相污染。常见的处理是只给主指标开面积其余保持纯线。堆叠折线图stack是另一个容易被误用的点。series: [ { name: A, type: line, stack: total, areaStyle: {}, data: [10, 20, 30] }, { name: B, type: line, stack: total, areaStyle: {}, data: [5, 15, 10] } ]stack值相同的系列会被累加图上一条线代表 B两条线之间的差值代表 A。这种图适合展示“总量由哪几部分构成”但不适合看单个系列的绝对值变化。如果业务方想同时看构成和单值我一般建议拆成上下两张图而不是硬塞在一张里。3.5 tooltip 与 legend 的实用配置tooltip在折线图里的触发方式基本固定用trigger: axis鼠标在任意点上都能看到同一横坐标下所有系列的值这是折线图的常规交互预期。内容长了会超出屏幕需要控制换行tooltip: { trigger: axis, appendToBody: true, // 避免被父级 overflow:hidden 裁掉 extraCssText: max-width:320px;white-space:normal;word-break:break-all;, formatter: (params) { const lines params.map( (p) ${p.marker}${p.seriesName}b${p.value}/b ) return div stylefont-weight:600;margin-bottom:4px${params[0].axisValue}/div${lines.join(br/)} } }appendToBody: true这一条特别值得记。当图表放在有overflow: hidden的卡片或者滚动容器里tooltip 很容易被裁掉一半挂在body上就彻底避免了这个问题。代价是 tooltip 的层级脱离组件树配合z-index需要留意。至于extraCssText里的white-space: normal就是「echarts tooltip 自动换行」这个需求的正确答案比在formatter里手动插\n更省事因为不同系列的文本长度不一样手动断点很难算准。legend部分icon用roundRect配上itemWidth: 12, itemHeight: 4会得到类似线条缩略图的效果比默认的方块更贴合折线图的语义。图例项一多就换行配合grid.top留出空间即可。4. 让数据动起来Vue 响应式与 ECharts 实例的协作静态图谁都会画真正的工程问题在数据更新上。这一节讲讲我在项目里总结的几条配合规则。4.1 不要用 v-if 包住图表容器这个坑我被坑过一次印象很深。需求是“数据加载完成后再显示图表”于是写了div v-ifhasData refchartRef。结果接口返回数据、hasData变true、DOM 创建、init执行——看起来一切正常。但第二次数据刷新时hasData短暂变false再变true比如 loading 状态切换原来的容器 DOM 被销毁重建ECharts 实例还绑在旧 DOM 上图表就白了。解决办法很简单容器常驻用v-show控制显隐或者干脆不管显隐只在数据为空的时候用 ECharts 的graphic组件画一个“暂无数据”的文字覆盖层。后者体验更好也避开了所有 DOM 生命周期问题。4.2 setOption 的合并行为必须搞清楚setOption(option)默认是合并模式不是替换。这条规则带来两个后果。第一个后果是series数组按索引合并。原来有 3 条线新数据只有 2 条如果不做处理第 3 条线会残留旧数据继续显示。第二个后果是option里没写的配置项不会被重置会保留上一次的值。处理方式按场景选// 场景一系列数量固定只更新数据 —— 最推荐性能最好 chartInstance.value.setOption({ series: [ { data: newDataA }, { data: newDataB } ] }) // 场景二系列数量会变比如渠道动态增删 chartInstance.value.setOption(buildOption(), { replaceMerge: [series] }) // 场景三整张图彻底重绘 chartInstance.value.setOption(buildOption(), { notMerge: true })日常最常用的是场景一只传series的数据进去ECharts 会做差异更新动画也是连续过渡的。场景二里replaceMerge: [series]是关键它表示“series这个字段整体替换其他字段照旧合并”是动态增删系列时最安全的选择。场景三比较重整图重绘会丢掉过渡动画非必要不用。4.3 实时折线图滑动窗口怎么写才不卡每隔几秒拉一次接口画折线最朴素的做法是每次把新数据push进去然后setOption全量数据。数据点少的时候没事跑两小时之后数组攒了几千个点每次重绘都卡一下。正确做法是维护一个固定长度的滑动窗口const MAX_POINTS 60 const timeData [] const valueData [] function pushPoint(time, value) { timeData.push(time) valueData.push(value) // 超出窗口就从头部裁掉 while (timeData.length MAX_POINTS) { timeData.shift() valueData.shift() } chartInstance.value?.setOption({ xAxis: { data: timeData.slice() }, series: [{ data: valueData.slice() }] }) }几个细节slice()是必要的直接传原数组引用可能让 ECharts 内部缓存和外部修改产生冲突MAX_POINTS取 60 到 120 之间比较合理对应“最近一小时”之类的业务口径同时记得把animation关掉或者缩短。animation: false // 或者保留轻微动画 animationDuration: 200, animationEasing: linear实时图上的动画如果太长曲线会一直在“追”新数据看起来像在抖动。我一般在监控大屏上用animation: false在后台趋势图上保留 200ms 左右的线性动画。另外如果刷新间隔很短小于 1 秒别用setInterval直接对着它做请求先做一层节流并且把上一次未完成的请求取消掉否则响应顺序错乱会导致折线出现倒着走的线段。4.4 大数据量降采样与 appendData折线图点数上到几千甚至上万时需要额外手段。series: [{ type: line, large: true, // 开启大数据量优化 sampling: lttb, // LTTB 降采样保留形状特征 showSymbol: false, animation: false, data: bigArray }]sampling: lttb是比较推荐的算法它会在保持曲线整体形态的前提下把点数降到画布能分辨的量级肉眼几乎看不出差别但渲染耗时能降一个数量级。large: true会切到更简单的渲染路径代价是部分交互和样式效果会失效所以只在确定数据量确实很大时才开。如果是持续追加的流式数据ECharts 还提供了appendData只往已有数据后面追加而不重算整条线。不过它对series类型和数据结构有要求配置起来比滑动窗口麻烦除非点数真的到了几万级否则我用得不多。5. 自适应、销毁与多图共存图表写完之后最容易在“上线之后”暴露问题的就是这几件事。5.1 用 ResizeObserver 代替 window.resize几乎所有教程都告诉你监听window.resize然后调chart.resize()。这在“窗口大小变化”的场景下够用但漏掉了两类高频情况侧边栏折叠导致容器变宽、卡片从隐藏状态变为显示导致容器从 0 变成正常尺寸。这两种情况下窗口尺寸没变window.resize不会触发图表就保持旧尺寸出现内容被拉伸或者右侧留白。ResizeObserver能直接观察容器元素本身的尺寸变化覆盖这两种场景。import { ref, shallowRef, onMounted, onBeforeUnmount } from vue import * as echarts from echarts const chartRef ref(null) const chartInstance shallowRef(null) let ro null let rafId null function handleResize() { // 用 rAF 做一次合帧避免连续触发时反复重排 if (rafId) cancelAnimationFrame(rafId) rafId requestAnimationFrame(() { chartInstance.value?.resize() }) } onMounted(() { chartInstance.value echarts.init(chartRef.value) chartInstance.value.setOption(buildOption()) ro new ResizeObserver(handleResize) ro.observe(chartRef.value) }) onBeforeUnmount(() { ro?.disconnect() ro null if (rafId) cancelAnimationFrame(rafId) chartInstance.value?.dispose() chartInstance.value null })这里加requestAnimationFrame是为了节流。拖拽窗口边缘的时候ResizeObserver会以极高频率触发每次都同步调resize()会明显掉帧合到下一帧执行就顺滑很多。注意不要在handleResize里做setOptionresize只负责重算画布尺寸和坐标轴布局。5.2 从隐藏状态切回来时的手动修复有些场景ResizeObserver也救不了容器一直是display: none切到可见的瞬间浏览器还没完成布局resize()拿到的是 0 宽度。典型是 Tab 切换、弹窗打开。稳妥的做法是在切换完成的nextTick之后再补一次resizeimport { nextTick } from vue async function onTabChange() { activeTab.value chart await nextTick() // 双保险再等一帧确保布局完成 requestAnimationFrame(() { chartInstance.value?.resize() }) }5.3 销毁不是可选项dispose()必须在组件卸载前调用。不调用的后果是ECharts 实例、它内部的 Canvas、事件监听、动画定时器全部留在内存里。单页应用里来回切几次路由内存就上去了图表多的话页面会越来越卡。我的习惯是把dispose和ResizeObserver.disconnect写在一起作为固定的清理清单。另外如果绑了自定义事件比如chart.on(click, ...)dispose会把实例自带的事件清掉但如果你手动往window上绑过监听那部分要自己removeEventListener。多图共存的情况还要注意一点如果页面上有五六张图同时初始化且每张图都绑了ResizeObserver窗口拖拽时会有明显的性能峰值。这时候可以做一个统一的图表注册表用一个观察者去批量处理 resize。6. 常见问题排查速查表下面这张表是我这几年攒下来的基本都是排查过一次就再也不想查第二次的问题。现象大概率原因处理方式图表区域一片空白控制台无报错容器高度为 0或父级height: auto给容器显式高度检查父级高度链init报 “Initialize failed: invalid dom”ref未绑定或v-if导致元素不存在确认在onMounted之后改用v-show窗口缩放后图表变形、右侧留白没监听 resize或监听了window.resize但容器变化不触发换用ResizeObserver观察容器数据更新后线条不动只改了本地数组没调setOption或改了响应式数据但图表不感知手动setOption传新数据系列数量减少后旧线还在setOption默认合并series按索引合并用replaceMerge: [series]x 轴标签挤成黑条容器宽度不够标签数量过多hideOverlap: true、formatter截短、必要时上dataZoomy 轴刻度出现 12.5、37.5 这类值只设了min/max没设interval三个一起设死双轴图里柱子被压成一条线两个量纲差异过大但共用一根轴或splitNumber不一致用双yAxisyAxisIndex两轴splitNumber对齐tooltip 被裁掉一半父级有overflow: hiddenappendToBody: truetooltip 文本超长不换行默认white-space: nowrapextraCssText里加white-space: normal和max-width页面用 pxtorem 做适配但图表字号不变ECharts 在 Canvas 内绘制不经过 CSSrem 不生效在 option 里按屏宽算字号或init后统一注入缩放路由切走后内存持续上涨未调用dispose卸载钩子里disposedisconnect平滑曲线掩盖了数据拐点smooth: true系数过大降到0.2或关闭看数据本身是否已被平滑大屏上文字发虚Canvas 未做 DPR 适配或容器尺寸非整数init时指定devicePixelRatio容器尺寸取整关于第 11 条pxtorem 对 ECharts 无效算是 Vue3 大屏项目里的高频问题。原因是 rem 是 CSS 层面的单位换算而 ECharts 的坐标轴文字、图例文字、tooltip 文字都是画在 Canvas 上的走的是自己的fontSize配置。常见的处理有两种一是在 option 生成时用一个基于窗口宽度的比例系数把axisLabel.fontSize、legend.textStyle.fontSize、title.textStyle.fontSize统一乘上去二是初始化时传入自定义的devicePixelRatio配合 CSS 的transform: scale()做整体缩放。第一种更可控我一般用第一种。7. 两个值得提前做掉的小优化7.1 按需引入把打包体积压下来全量import * as echarts from echarts大概会往产物里塞进几百 KB。如果项目里只用到折线图和柱状图完全可以按需引入。// src/utils/echarts.js import * as echarts from echarts/core import { LineChart, BarChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent, TitleComponent, DataZoomComponent, MarkLineComponent } from echarts/components import { CanvasRenderer } from echarts/renderers import { UniversalTransition } from echarts/features echarts.use([ LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, DataZoomComponent, MarkLineComponent, CanvasRenderer, UniversalTransition ]) export default echarts之后业务组件里统一从这个文件引入echarts。有个小坑echarts.graphic.LinearGradient在按需引入的模式下依然可用但像echarts.registerMap、echarts.registerTheme这类方法要确保对应的模块已经use进来否则运行时会报未定义。我遇到过一次地图组件没引入MapChart报错信息很不直观查了半天。7.2 导出图片与主题切换导出功能在报表类项目里几乎必有function exportPng() { const url chartInstance.value.getDataURL({ type: png, pixelRatio: 2, // 2 倍图投屏和打印都够用 backgroundColor: #ffffff // 默认透明导出成图片后底色可能是黑的 }) const a document.createElement(a) a.href url a.download 趋势图_${Date.now()}.png a.click() }backgroundColor那一项务必显式设置。默认导出的 PNG 背景是透明的丢进 PPT 或者微信里在深色底上就会变成一团看不清的东西。主题方面ECharts 内置了dark主题echarts.init(el, dark)就能切换。但内置暗色主题的配色比较固定和业务主色不搭的时候更实用的做法是echarts.registerTheme注册一份自定义主题或者干脆在 option 里显式写好深色下的textStyle.color、splitLine.lineStyle.color。我偏向后者虽然啰嗦但每个颜色的来历都清楚不用去翻主题文件。关于折线图的优化其实还有空间可以挖比如用markLine画目标基线、用markArea标出异常时段、用dataZoom做时间范围筛选这些都可以在已有的series配置上直接叠加不需要重构代码。