ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue3+ECharts 数据可视化:七步避坑与 useEcharts 封装

Vue3+ECharts 数据可视化:七步避坑与 useEcharts 封装 做后台管理系统和数据可视化大屏的人几乎绕不开 Vue3 加 echarts 这套组合。前者负责把状态、路由、交互理顺后者负责把数据画成人一眼能看懂的样子。这两个东西本身都不难难的是把它们粘到一起时那些说大不大、说小不小的坑图表死活不显示、控制台报实例已经存在、切个 Tab 回来图就变形了、窗口缩放图表不跟着动、页面销毁了内存还在涨。我见过太多人卡在第一步——容器明明写了echarts.init 也调了页面上就是一片空白然后开始怀疑是不是版本不兼容。其实九成以上都不是版本问题是顺序和细节的问题。这篇就按我自己在项目里反复用的七个步骤往下走每一步说清楚为什么这么做哪里最容易翻车怎么提前绕开。不管你是刚上手 Vue3 的新手还是做了几年想把这套东西沉淀成公共组件的老手都能从里面捞到点能直接抄的东西。1. 动手之前先把版本和引入方式定下来很多人一上来就npm install echarts装完发现项目里已经有别的依赖版本冲突或者引了一整包两兆多的库打包体积直接起飞。这一步花十分钟想清楚能省后面半天折腾。1.1 Vue3 工程环境与 echarts 版本选择先说工程侧。现在新建 Vue3 项目主流就两条路vite和vue/cli。我的建议是能用 Vite 就用 Vite配置少、冷启动快、HMR 几乎无感和 echarts 这种纯前端库配合起来没有额外心智负担。用 Vite 建带 TypeScript 的模板一条命令搞定npm create vitelatest my-chart-app -- --template vue-ts cd my-chart-app npm install npm install echarts --save这里有个细节值得说Windows 环境下如果你用 PowerShell--后面那串参数有时候会被吞掉导致模板没选上一路回车建出来一个空壳。稳妥一点的做法是把命令拆开写或者干脆用 cmd。这个坑不常见但真有人踩建完发现目录里只有package.json。版本方面echarts 5.x 是当前稳定主线Vue3 对它的兼容没有问题。需要留意的是如果你项目里还留着 Vue2 的老页面在跑别图省事把两个框架塞进同一个仓库共享一份 echarts实例的注册状态是全局的按需引入的注册在 A 页面做过了B 页面不注册也能用看起来很美好实际上换个构建环境就崩了。这种隐式依赖是后期最难查的问题之一。还有一个容易被忽略的点——TypeScript 项目里 echarts 的类型从 5.0 开始已经内置不需要额外装types/echarts。装了反而可能因为类型定义版本不对出现 option 里明明合法的属性被标红。见到XX 不是 EChartsOption 的属性这类报错先去package.json里翻翻有没有多装的类型包。1.2 全量引入还是按需引入别拍脑袋这是第一个真正的分叉口。全量引入的写法就是一行import * as echarts from echarts好处是省心option 里写什么图表类型都不用管echarts.init直接能用。代价是打包体积。echarts 全量包压缩后大概在一兆上下如果你的项目只是后台管理系统里画三四张折线图和饼图这一个库的体积可能比整个业务代码还大。对于首屏加载敏感的大屏项目这一兆实打实影响白屏时间。按需引入要写一坨注册代码但体积能砍到几百 KB 甚至更少// utils/echarts.ts import * as echarts from echarts/core import { LineChart, BarChart, PieChart, MapChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent, TitleComponent, DataZoomComponent, ToolboxComponent, VisualMapComponent, GeoComponent } from echarts/components import { LabelLayout, UniversalTransition } from echarts/features import { CanvasRenderer, SVGRenderer } from echarts/renderers echarts.use([ LineChart, BarChart, PieChart, MapChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, DataZoomComponent, ToolboxComponent, VisualMapComponent, GeoComponent, LabelLayout, UniversalTransition, CanvasRenderer, SVGRenderer ]) export default echarts这段代码里的每一项都要对应上你实际用到的东西这是最容易出问题的地方。漏注册组件不会在构建时报错只会在运行时图表少一块——比如你做了个折线图带 tooltip但忘了TooltipComponent鼠标划上去一点反应都没有排查半天以为是事件绑定问题。所以我的习惯是新建这个文件时先把常用的都注册上等项目稳定了再回头做减法用构建分析工具看看到底哪些没被用到。另外提一句渲染器的选择。默认是 Canvas绝大多数场景够用。如果图表要支持高倍屏下的文字极致清晰、或者要用 CSS 给图表里的元素做动画可以考虑 SVGRenderer它输出的是真实 DOM 节点方便调试但节点一多性能会掉。数据量大、节点多的场景老老实实用 Canvas。提示按需引入的文件建议单独放一个模块全项目统一从这里导入 echarts。分散在各个组件里各自注册很容易出现这个页面能用那个页面不能用的诡异现象。2. 七步走从空文件到第一张能看的图前面铺垫完正式进入七步。这七步不是凑数是每次写图表组件都绕不开的完整闭环少任何一步都会在某个场景下出问题。2.1 第一步与第二步装依赖、把需要的模块注册进来第一步安装依赖。npm install echarts如果走按需引入第二步就是把上面那个utils/echarts.ts建好。这两步没什么花头但有一个检查点装完去package.json确认版本号落到了dependencies而不是devDependencies有些包管理器的交互式安装会把运行时依赖装到开发依赖里本地跑没事CI 构建上线就报模块找不到。第二步注册模块并导出统一的 echarts 实例。这里我要补充一个实际项目里的经验。如果你用的是echarts/core那么echarts.init依然存在但echarts.graphic、echarts.registerMap这些扩展 API 也都在不用担心按需引入之后功能被砍。真正的区别只在于图表类型和组件必须显式注册。关于地图尤其是中国地图需要单独说一下。echarts 5 之后官方包里不再自带地图 geoJSON 数据你需要自己准备地图数据文件然后调用echarts.registerMap(china, geoJson)注册。这个 geoJSON 来源要自己去获取并遵守相应授权。注册完之后geo或series.type map才能用。很多人搜到老教程里写echarts/map/js/china.js然后 import在新版本里这个路径已经不存在了报错找不到模块就是因为这个。2.2 第三步到第五步容器、实例、配置项第三步准备一个带明确宽高的 DOM 容器。这一步是整个七步里翻车率最高的没有之一。template div refchartRef classchart-container/div /template style scoped .chart-container { width: 100%; height: 400px; } /style两个硬性要求宽度有来源高度有显式值。echarts 初始化时会去读容器的clientWidth和clientHeight如果高度是auto读出来是 0它就按 0 高去初始化画布高 0你什么都看不见控制台还一声不吭。宽度用100%没问题但要往上追一层——父元素必须有确定宽度。我遇到过有人把图表放在display: flex的容器里子项没设flex: 1也没设宽度父容器宽度是内容撑开的内容又是图表死循环结果宽度算出个 0。高度用什么单位都行px、vh、calc()都可以就是不能auto或者百分比且父级没高度。用vh做大屏是常见做法注意移动端浏览器地址栏收起展开会改变vh的计算值图表高度会跳一下这时候要在 resize 里补一次重算。第四步在 onMounted 里初始化实例。为什么要放onMounted因为onMounted之前 DOM 还没挂载ref拿到的是null。这是 Vue3 组合式 API 里最基础的时序知识但很多人写setup顶层就直接 init报无法读取 null 的属性然后把锅甩给 echarts。import { ref, onMounted, onBeforeUnmount, shallowRef } from vue import echarts from /utils/echarts import type { ECharts } from echarts/core const chartRef refHTMLDivElement | null(null) const chartInstance shallowRefECharts | null(null)注意这里我用的是shallowRef而不是ref。这是个非常重要的点值得展开说。Vue3 的ref会把它包的值转成响应式代理对象。echarts 实例内部结构极其复杂有大量循环引用和内部状态被 Proxy 包一层之后轻则性能下降重则某些内部方法调用时this指向出问题图表渲染异常但报错信息毫无提示。用shallowRef只对.value本身的赋值做响应内部不代理问题直接消失。等价的做法还有markRaw(echartsInstance)。记住一句话第三方类实例、地图实例、编辑器实例进 Vue3 一律shallowRef或markRaw。第五步写好 option 并调用 setOption。const option { tooltip: { trigger: axis }, legend: { data: [访问量, 下单量] }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, boundaryGap: false, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日] }, yAxis: { type: value }, series: [ { name: 访问量, type: line, smooth: true, data: [820, 932, 901, 934, 1290, 1330, 1320] }, { name: 下单量, type: line, smooth: true, data: [120, 200, 150, 180, 260, 300, 280] } ] }setOption的第二个参数是个经常被忽略但有实际影响的开关。默认是合并模式你第二次调用setOption传的新配置会和旧配置做深度合并。这个行为在数据更新时通常是好事但有两种情况必须传truenotMerge一是切换图表类型比如从折线图切成柱状图旧配置里的series是 line 类型合并模式下可能残留旧的轴配置出现双 Y 轴或者多余的图例项。二是数据条数变少的时候合并模式不会删除多余的元素。我见过一个真实案例图表按筛选条件动态展示不同数量的系列用户从选 10 条改成选 3 条图上还是 10 条因为合并模式保留了旧系列。改成setOption(option, true)立刻正常。反过来频繁全量替换的代价是每次都要重建整个图表动画会重放。所以数据更新时候的取舍是结构变了用 notMerge只是数值变了用合并。2.3 第六步与第七步自适应与销毁第六步监听尺寸变化调用 resize。容器尺寸变化了echarts 不会自己知道。它初始化时记下了画布的宽高之后你不主动告诉它它就一直按老尺寸画表现出来就是图要么被裁掉一半要么周围一圈空白。const handleResize () { chartInstance.value?.resize() } onMounted(() { window.addEventListener(resize, handleResize) })window的 resize 只能覆盖浏览器窗口变化的场景。侧边栏折叠展开、Tab 切换、弹窗打开这些都不会触发 window resize但容器宽度实实在在变了。这是第二个高频翻车点。解决办法放在第 4 章详细说用ResizeObserver。第七步组件卸载前销毁实例、解绑监听。onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chartInstance.value?.dispose() chartInstance.value null })dispose不只是清空画布这么简单它会释放内部的事件监听、定时器、canvas 上下文。不做这一步页面来回切换几十次内存就会持续上涨在大屏这种常驻页面里尤其明显。用onBeforeUnmount而不是onUnmounted。这两个钩子的区别是执行时机前者在组件销毁前、DOM 还在后者在销毁后。对于dispose来说两者都能用但如果你需要在销毁前先读一下 DOM 尺寸做点什么onBeforeUnmount更保险。注意dispose之后再调setOption会报错。如果你的异步请求回来得比组件销毁晚一定要在请求回调里判断实例是否还存在否则一个切页面就报错的 bug 就诞生了。3. 把七步封装成可复用的组合式函数七步走完一张图出来了。但如果项目里有二十个图表组件每个都重复这七步代码量和出错概率都会指数上升。这一章讲怎么把它封成一个useEcharts让业务组件只关心 option。3.1 useEcharts 的完整实现// composables/useEcharts.ts import { onBeforeUnmount, onMounted, shallowRef, watch, type Ref } from vue import echarts from /utils/echarts import type { ECharts, EChartsCoreOption } from echarts/core export function useEcharts( elRef: RefHTMLElement | null, option: RefEChartsCoreOption ) { const instance shallowRefECharts | null(null) let resizeObserver: ResizeObserver | null null const init () { if (!elRef.value) return // 防止同一容器重复初始化 const exist echarts.getInstanceByDom(elRef.value) if (exist) { instance.value exist } else { instance.value echarts.init(elRef.value) } instance.value.setOption(option.value) } const resize () instance.value?.resize() onMounted(() { init() if (elRef.value typeof ResizeObserver ! undefined) { resizeObserver new ResizeObserver(() resize()) resizeObserver.observe(elRef.value) } window.addEventListener(resize, resize) }) // option 变化时自动更新图表 watch( option, (val) { instance.value?.setOption(val) }, { deep: true } ) onBeforeUnmount(() { window.removeEventListener(resize, resize) resizeObserver?.disconnect() resizeObserver null instance.value?.dispose() instance.value null }) return { instance, resize } }几处设计说明。第一echarts.getInstanceByDom这个 API 是防重复初始化的关键。Vue 的KeepAlive缓存组件时onMounted可能在激活时被再次触发如果直接init控制台就会打出那句经典的There is a chart instance already initialized on the dom。用这个 API 先查有没有有就复用。第二watch 加deep: true是为了应对直接改 option 内部属性的写法。如果业务侧习惯整体替换 option 对象可以把 deep 去掉省点性能。用ResizeObserver而不是直接调 resize还有一个好处它能感知到容器被隐藏又显示的变化。比如 Tab 切换时容器从display: none变成display: block宽度从 0 变成实际值ResizeObserver 会触发一次回调图表自动重算。省掉了在 Tab 切换事件里手动调 resize 的麻烦。不过要注意一个反直觉的现象如果图表所在容器一开始就是隐藏的比如弹窗里的图表init时读到宽度是 0echarts 会按 0 宽初始化画出来一坨。等容器显示出来ResizeObserver 触发 resize理论上能救回来但有些版本的 echarts 在宽为 0 初始化后 resize 效果不理想。稳妥做法是等容器真正可见再初始化在弹窗的opened事件里或者用v-if加nextTick控制。3.2 TypeScript 项目里的类型约束与 props 设计如果业务组件想做成一个通用的BaseChart :optionxxx /props 和类型要这么设计script setup langts import { toRef } from vue import type { EChartsCoreOption } from echarts/core import { useEcharts } from /composables/useEcharts const props withDefaults( defineProps{ option: EChartsCoreOption height?: string autoResize?: boolean }(), { height: 360px, autoResize: true } ) const chartRef refHTMLDivElement | null(null) useEcharts(chartRef, toRef(props, option)) /script template div refchartRef :style{ width: 100%, height }/div /template这里toRef(props, option)的写法比computed(() props.option)更直接。注意不要用解构const { option } props那样会丢失响应性——这是 Vue3 里说了一万遍但每天还是有人踩的坑。关于EChartsCoreOption这个类型如果你走的是按需引入用它才准确。全量引入的话可以用EChartsOption。这两个类型对某些属性的约束不完全一样混用会报类型错误比如series在某些类型下不认自定义的扩展字段。提示defineProps里的option如果传的是一个大对象父组件每次渲染都会生成新引用触发子组件 watch 更新。如果 option 内容其实是稳定的用computed缓存一下能少很多无意义的setOption调用。4. 尺寸自适应的三种真实场景resize 这件事看起来简单实际项目里能拆出好几种不同的触发场景每种的处理方式略有差异。搞清楚了图表就不会再出现半张图或者变形。4.1 窗口缩放、侧边栏折叠、Tab 切换的差异场景一浏览器窗口拖动缩放。最基础的一种window.resize监听就够了。但拖动过程中这个事件触发极其频繁一次拖动可能触发上百次每次都调resize()会导致卡顿。所以必须加防抖我一般设 100 到 200 毫秒。场景二后台管理系统的侧边栏折叠。左侧菜单栏从展开变成收起主内容区宽度瞬间变大但窗口尺寸没变window.resize完全不触发。这个场景下只能用ResizeObserver监听图表容器本身。场景三Tab 页切换。用户在多个 Tab 之间来回切每个 Tab 里都有一张图。如果用的是v-show被隐藏时容器宽高都是 0ResizeObserver 会触发一次resize()把图表算成 0 尺寸。切回来时再触发一次重新算回正常。看起来没问题但因为宽度中途变成了 0图表的grid、legend布局会重新计算有时候会留下明显的闪烁。这种情况我更推荐v-if按需挂载切到哪个 Tab 才渲染哪个切走就销毁配合前面的销毁逻辑内存也干净。如果是el-tabs这类组件有时候切换后容器宽度是渐变动画ResizeObserver 会在动画过程中连续触发导致图表一直重算看起来很晃。加个 150ms 的防抖就平滑了。4.2 防抖封装与尺寸兜底防抖函数我喜欢自己写一个小的不引 lodashexport function debounceT extends (...args: any[]) void(fn: T, delay 150) { let timer: ReturnTypetypeof setTimeout | null null return (...args: ParametersT) { if (timer) clearTimeout(timer) timer setTimeout(() { fn(...args) timer null }, delay) } }配合 ResizeObserver 使用const doResize debounce(() { instance.value?.resize({ animation: { duration: 0 } }) }, 150) resizeObserver new ResizeObserver(doResize)resize方法可以传参animation: { duration: 0 }表示缩放过程中不播动画。这个细节很重要——默认情况下每次 resize 都会触发一次完整的入场动画用户拖窗口的时候图会一直在抖。关掉动画后缩放就是纯粹的尺寸变化视觉上干净很多。还有一个兜底技巧在 resize 之前检查容器尺寸是否为 0。const safeResize () { if (!elRef.value) return const { clientWidth, clientHeight } elRef.value if (clientWidth 0 || clientHeight 0) return instance.value?.resize() }这么做的好处是避免容器被隐藏时把图表的内部尺寸记录覆盖成 0。虽然理论上再显示时会恢复但实际中我遇到过隐藏-显示后图表尺寸锁死在很小的值的情况加了这道检查就再没出现过。5. 高频坑位与排查速查表这一章把我在项目里真实碰到过的问题整理出来方便你对照排查。有些问题现象一样但原因完全不同别看到图不显示就往版本上想。5.1 图表空白、实例重复、内存泄漏空白第一类容器没有高度。现象是 DOM 上有 canvas 元素但高度是 0 或者几十像素。打开开发者工具选中容器看计算后的高度是 0 就是这个问题。解决方式是给容器显式高度或者检查父级链路上有没有height: auto。空白第二类初始化时机早于 DOM 挂载。现象是控制台报无法读取 null 的属性。检查 init 是否包在onMounted或nextTick里。空白第三类按需引入漏注册。现象是图表有坐标轴但没有数据系列或者干脆什么都没有。检查echarts.use([...])里有没有注册对应的 Chart 和 Component。空白第四类数据格式不对。比如xAxis.data给的是对象数组而不是字符串数组或者 series 的data是[{value: 1}, ...]这种嵌套结构但类型标错了。这种情况图表框架在就是没内容。报错实例已经存在。原因是同一个 DOM 容器被init了两次。常见触发场景是KeepAlive缓存、或者 HMR 热更新时旧实例没销毁。解决办法是用getInstanceByDom先判断或者在onMounted之前先dispose一次。内存持续增长。检查三点dispose有没有调、window事件有没有解绑、ResizeObserver有没有disconnect。这三个少任何一个都会泄漏。还有一个隐蔽的如果图表里用了setInterval做数据轮询组件销毁时忘了clearInterval定时器会一直跑下去每跑一次还往一个已经销毁的实例上setOption报错刷屏。5.2 pxtorem 失效、tooltip 换行、标签偏移pxtorem 对 echarts 没效果。这个问题很典型。你用 postcss-pxtorem 做了移动端适配CSS 里的 px 都转成了 rem但图表里的字号、间距全都不变。原因很简单postcss 只处理 CSS 文件里的 pxecharts 的尺寸全是在 JS 的 option 里用数字写的走的是 canvas 绘制根本不经过 CSS 管线。解决办法是手动换算。设计稿按 375 宽、根字号 16px 来算图表里想用 14px 的字就写成const rootFontSize parseFloat(getComputedStyle(document.documentElement).fontSize) const fontSize (14 / 16) * rootFontSize const option { xAxis: { type: category, axisLabel: { fontSize } } }然后在 resize 的时候重新计算一遍 fontSize 并setOption更新。这样改的好处是跟 CSS 的 rem 缩放保持同一个节奏不会出现文字比布局大一圈的错位。tooltip 内容太长不换行。默认情况下 tooltip 是一行撑开内容长了就跑出屏幕。它内部其实支持 HTML但你得告诉它允许换行tooltip: { trigger: axis, extraCssText: white-space: normal; word-break: break-all; max-width: 320px;, formatter: (params) { return params.map(p ${p.seriesName}${p.value}br/).join() } }关键在extraCssText里那个white-space: normal。默认值是nowrap不加它你写多少个br/都不生效。max-width配合word-break保证超长内容自动折行而不是横向溢出。饼图 labelLine 末尾小圆点位置偏移。饼图的引导线分两段length是第一段直线长度length2是第二段水平直线的长度样式里的symbol控制末尾的小圆点。偏移通常是因为label的align和labelLine的长度没配合好左侧的标签要右对齐、引导线向左延伸右侧的标签左对齐、引导线向右延伸这个方向由label.align自动判断但如果你手动写了固定的align就会出现尺寸变化时圆点飘走。稳妥写法是不写死 align只调labelLine.length和length2两个数值让 echarts 自己判断方向。为了方便对照我把常见现象和根因整理成一张表现象最可能的原因处理方式图表区域一片空白容器高度为 0 或 auto给容器显式高度检查父级宽度链路控制台报 null 属性init 早于 DOM 挂载移入 onMounted 或用 nextTick提示实例已初始化同容器重复 initgetInstanceByDom 判断后复用图有轴无数据按需引入漏注册 Chart检查 echarts.use 数组侧边栏折叠后图变形只监听了 window.resize改用 ResizeObserver拖窗口时图抖动resize 频繁触发且带动画加防抖resize 时关闭动画页面切换内存上涨未 dispose 或事件未解绑补齐销毁三件套图表字号不随 rem 缩放postcss 不处理 canvas手动按根字号换算 fontSizetooltip 内容不换行white-space 默认 nowrapextraCssText 设置 normal数据变少了图没变setOption 合并模式残留传 true 使用 notMerge6. 往深走一步数据更新、主题与大数据量七步和封装都跑通了图表能稳定显示了。这时候项目里通常会出现新的诉求数据要实时刷新、要支持明暗主题切换、数据量上万条要卡。这一章聊聊这些。6.1 数据更新策略与 notMerge 的取舍图表数据来源一般有两种一是接口一次性拉回来二是有个定时器轮询或者 WebSocket 推送。对于第一种watch到数据变化后换个 option 全量setOption就行简单直接。对于第二种如果每次推送都全量替换 option数据量大时会有明显卡顿因为整棵配置树都要重新解析。这种情况下有个更讨巧的做法只更新 series.data。// 假设 instance 已存在结构没变只有数值变了 instance.value?.setOption({ series: [{ data: newData }] })合并模式下echarts 只更新对应的数据字段不会重建坐标轴和图例动画也是平滑过渡而不是重播。对于每秒刷新一次的实时监控曲线这个差异非常明显。但要注意我前面说过合并模式会保留旧数据。如果新数据的条数比旧数据少图表上会残留。处理办法是在更新前把数组长度对齐或者干脆在这次更新时给series多加一个name标识确保匹配到正确的系列。乱序的 series 数组是另一个常见坑合并模式下 echarts 按索引匹配如果你 update 时 series 顺序变了数据就会串到别的系列上颜色也跟着变看起来像数据错了实际上是顺序错了。关于主题切换echarts 支持init的第二个参数传主题名。切换暗色主题时很多人直接重新init这么做会把实例销毁重建所有状态丢失动画重放还会有短暂的空白。更好的方式是提前注册好两套主题切换时用instance.setOption(option)更新颜色相关配置或者利用 echarts 的主题能力在初始化时就绑定然后通过更新 option 里的颜色字段来切换。数据量不大的话直接dispose再init也不是不行但要接受那一瞬间的白。6.2 大屏场景下的性能处理数据可视化大屏有几个典型特征全屏、常驻不刷新、图表多、动画多。这套组合对性能不友好。第一条经验是关掉不必要的动画。大屏上图表多每个图表入场动画都是 1 秒整个屏幕同时动起来会明显掉帧。把animation: false或者把animationDuration调到 300 毫秒以内观感反而更利落。第二条是用 dataZoom 或者降采样处理大数据量。折线图超过几千个点之后屏幕上根本分辨不出来纯属浪费算力。用sampling: lttb让 echarts 自动降采样到画面能显示的密度视觉几乎无损性能提升明显series: [{ type: line, sampling: lttb, data: hugeArray }]第三条是多图表共用渲染器要谨慎。有些项目为了省内存会尝试共用 canvas实际上 echarts 每个实例有独立的渲染上下文强行共享会出问题。正确的省内存方式是控制同时存在的实例数量比如大屏上的图表分批初始化或者超出视口的图表先不渲染。第四条是分辨率。大屏经常跑在 1920 以上分辨率echarts.init时传devicePixelRatio可以控制渲染清晰度默认取window.devicePixelRatio。如果大屏机器上这个值异常高比如接了高分屏canvas 尺寸会成倍增长显存吃紧。可以在 init 时手动限制成 2 以内echarts.init(el, null, { devicePixelRatio: Math.min(window.devicePixelRatio, 2) })这条我是在一个 4K 大屏项目里实测出来的。当时图表一动就卡排查了半天数据量最后发现是 devicePixelRatio 是 3画布实际分辨率是容器尺寸的 9 倍GPU 压力巨大。限制成 2 之后流畅度立刻上来了。最后分享一个我自己踩了不止一次的坑定时刷新和组件生命周期的竞态。轮询接口回来的时候组件可能已经被销毁了或者用户已经切到了别的筛选条件。如果不做判断直接把数据塞进图表轻则报错重则把旧数据渲染到新图表上看起来像数据错乱其实是异步时序问题。我的做法是在请求发出前记一个自增的请求序号回调里比对序号只有最新的那次才允许更新图表let reqId 0 const fetchData async () { const current reqId const res await api.getData() if (current ! reqId) return instance.value?.setOption({ series: [{ data: res.data }] }) }这段代码不长但能挡掉一大类看起来很玄学的 bug。图表这个东西渲染逻辑本身很稳绝大多数问题都出在数据到达的时机和容器状态的配合上把这两件事管住剩下的就只是调样式了。
返回列表