事情是这样的。上周我负责的一个后台管理系统,用户反馈说页面加载慢得要命,尤其是某个数据报表页面,打开要等七八秒,鼠标点一下筛选条件,又要转半天圈圈。我一开始还嘴硬,觉得可能是用户网络不行,结果自己拿Chrome DevTools一测,Network面板里那个DOMContentLoaded事件,硬是等了4.2秒,Load事件更是飙到6.8秒。这数据摆出来,脸都疼。
先说说这个页面的基本情况。Vue 2.6.14 + Element UI 2.15.6,数据报表页面大概有十几个筛选组件,一个表格,三个图表(ECharts 5.3.2),还有一些统计卡片。问题就出在,这个页面是用户登录后默认跳转的第一个页面,等于说首屏就是这个报表页。当时开发的时候图省事,所有组件都是全局注册,所有第三方库都是完整引入,连没用到的图表类型都打包进去了。
第一件事,我要搞清楚到底什么东西拖慢了速度。打开Chrome的Performance面板,录了个加载过程。看到那个长长的红色任务块了吗?一个叫做”chunk-vendors.js”的家伙,整整2.3MB,加载花了1.8秒,解析执行花了2.4秒。点进去一看,好家伙,ECharts完整包占了将近800KB,Element UI整个库占了600多KB,还有一堆lodash、moment.js这些工具库。我甚至在里面看到了moment的locale文件,明明我只用中文,它把全世界几十种语言都打包进来了。
这就是第一个坑:默认的打包配置,会把所有依赖都塞进一个vendor文件里。而且Vue CLI创建的项目,默认没开任何代码分割。
我做的第一项优化是路由懒加载。Vue Router支持动态import,这玩意儿大家应该都知道,但很多人懒得弄。原来的写法是:
“`javascript
import ReportPage from ‘@/views/ReportPage.vue’
“`
改成:
“`javascript
const ReportPage = () => import(/* webpackChunkName: “report” */ ‘@/views/ReportPage.vue’)
“`
就这么一行改动,效果立竿见影。首屏不再需要加载ReportPage组件的代码了,而是等用户真正访问这个路由的时候才去加载。但是注意,这个页面本身就是默认首页,所以懒加载其实只是把加载时机往后挪了一点点,真正的收益在于:如果用户从其他页面跳过来,就不需要重新加载整个应用了。
真正的重头戏是第三方库的按需加载。Element UI官方文档说用babel-plugin-component可以实现按需加载,我照着配了。但有个细节容易踩坑:配置完之后,记得把main.js里那个全局引入的`import ElementUI from ‘element-ui’`删掉,否则按需加载没效果。我一开始就是忘了删,还在那纳闷为啥打包体积没变。
ECharts这块更折腾。我原来用的是`import echarts from ‘echarts’`,这是完整包。改按需加载需要这样:
“`javascript
import * as echarts from ‘echarts/core’;
import { BarChart, LineChart, PieChart } from ‘echarts/charts’;
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from ‘echarts/components’;
import { CanvasRenderer } from ‘echarts/renderers’;
echarts.use([BarChart, LineChart, PieChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, CanvasRenderer]);
“`
这段代码看起来啰嗦,但效果很猛。ECharts从800KB降到了大概200KB左右。不过有个坑:如果你的图表用了某些比较少见的特性,比如地图或者3D效果,别忘了额外引入对应的组件,否则页面会静默失败,控制台也不报错,图表就是渲染不出来。我就因为这个排查了半天,最后发现是仪表盘图表没引入GaugeChart。
然后是moment.js的问题。这个库已经被无数人吐槽过了,但我之前一直懒得换。它确实大,而且有很多历史包袱。我决定换成dayjs,API几乎一模一样,但体积只有2KB。替换过程很简单,把所有的`moment()`换成`dayjs()`,格式化的方法名也是一样的。唯一要注意的是,dayjs默认不支持`moment().format(‘YYYY年MM月DD日’)`这种中文格式,需要加载一个插件:
“`javascript
import dayjs from ‘dayjs’;
import ‘dayjs/locale/zh-cn’;
import advancedFormat from ‘dayjs/plugin/advancedFormat’;
dayjs.extend(advancedFormat);
dayjs.locale(‘zh-cn’);
“`
搞定之后,moment.js就从打包清单里消失了,又省了将近300KB。
接下来我动了Webpack配置。Vue CLI的项目,可以在vue.config.js里配置splitChunks。我想要的效果是:把那些不太变动的大库,单独拆成独立的chunk,利用浏览器缓存。配置大概是这样的:
“`javascript
configureWebpack: {
optimization: {
splitChunks: {
chunks: ‘all’,
cacheGroups: {
echarts: {
name: ‘chunk-echarts’,
test: /[\\/]node_modules[\\/]echarts[\\/]/,
priority: 10
},
elementUI: {
name: ‘chunk-element-ui’,
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
priority: 10
},
commons: {
name: ‘chunk-commons’,
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}
“`
这么搞之后,打包出来的文件从原来一个巨大的vendor.js,变成了若干个几百KB的小chunk。首屏加载的时候,浏览器可以并行下载这些文件,而不是等一个2.3MB的文件慢慢传完。
但要注意priority的优先级设置,如果多个cacheGroup匹配同一个模块,优先级高的生效。我一开始没设priority,结果ECharts被误分到了commons组里,导致commons文件变得特别大,等于白优化了。
做完这些之后,我重新测了一下加载时间。DOMContentLoaded降到了1.1秒,Load事件降到了2.3秒。但说实话,体感上还是有点慢,尤其是切换筛选条件的时候,页面会卡一下。
这时候我才意识到,问题不只是加载,还有渲染性能。那几个ECharts图表在数据更新时会重新渲染,而Element UI的表格在数据变化时也会触发重排重绘。我用Chrome的Performance面板录了一次筛选操作,发现有一个函数叫`updateCharts`,执行了整整1.8秒。点进去看,原来是每次筛选条件变化时,我都会重新创建ECharts实例,而不是调用`setOption`更新数据。
原来的代码大概是这样的:
“`javascript
watch: {
filterData: {
handler(newVal) {
this.chartInstance = null;
this.$nextTick(() => {
this.initChart(newVal);
});
},
deep: true
}
}
“`
每次都是销毁再重建,这操作在ECharts里是大忌。正确的做法是在组件初始化时创建一次实例,后续只调用`setOption`:
“`javascript
mounted() {
this.chartInstance = echarts.init(this.$refs.chart);
this.chartInstance.setOption(this.buildOption(this.filterData));
},
watch: {
filterData: {
handler(newVal) {
this.chartInstance.setOption(this.buildOption(newVal), true);
},
deep: true
}
},
beforeDestroy() {
this.chartInstance && this.chartInstance.dispose();
}
“`
注意`setOption`的第二个参数传了`true`,表示不合并之前的配置,直接替换。如果不传这个参数,ECharts会尝试合并新旧配置,有时候会导致动画异常或者数据残留。
表格的卡顿是另一个问题。Element UI的el-table在数据量大的时候,默认会渲染所有行。我这个报表页面的表格大概有300行数据,不算特别多,但每行有十几个列,而且有些列里还嵌套了el-tag、el-button这些组件,导致DOM节点爆炸。解决方案是用虚拟滚动,但Element UI官方不直接支持,得自己搞或者用第三方库。我选了个轻量的方案:用`el-table`的`max-height`加上`el-table-column`的`show-overflow-tooltip`,让表格变成固定高度滚动,同时把那些嵌套组件改成纯文本渲染,只有在鼠标悬停时才显示操作按钮。这样DOM节点数从原来的4000多个降到了不到1000个。
还有一个容易被忽视的点是图片。这个报表页面顶部有个背景图,是一张2MB的PNG。我把它压缩成WebP格式,大小降到了200KB,然后在CSS里用`image-set`做兼容:
“`css
.background-banner {
background-image: url(‘banner.webp’);
background-image: -webkit-image-set(url(‘banner.webp’) 1x, url(‘banner@2x.webp’) 2x);
background-image: image-set(url(‘banner.webp’) 1x, url(‘banner@2x.webp’) 2x);
}
“`
旧浏览器不认识WebP的话,会回退到PNG,但至少现代浏览器能享受到体积优势。
最后我还做了一件事:把那些不需要首屏渲染的组件,比如底部的版权信息、侧边栏的版本号,用`v-if`控制只在页面完全加载后才显示。这听起来有点投机取巧,但确实能让用户感觉页面加载更快——先看到主要内容,其他东西慢慢出来,比一次性全部渲染完再显示要好得多。
全部改完之后,我又用Lighthouse跑了一次评分。从原来的Performance 42分,变成了88分。首屏加载时间从4.2秒降到了1.3秒。用户反馈说”现在快多了”,虽然我心里知道还有优化空间,比如Service Worker缓存、CDN加速这些还没搞,但至少这个版本能交差了。
踩坑经验总结一下:
– 别信”先写完再优化”这句话,一开始就要考虑打包策略,不然后面重构很痛苦。
– 按需加载第三方库时,记得删掉全局引入,否则没有效果。
– ECharts的按需引入要检查所有用到的图表类型和组件,漏一个就静默失败。
– splitChunks的priority一定要设,否则分组逻辑会乱掉。
– 图表更新用setOption,不要销毁重建,性能差很多倍。
– 表格卡顿先看DOM节点数,太多的话考虑虚拟滚动或者减少嵌套组件。
– 图片压缩是最容易忽略但收益最高的优化之一,WebP能省一大半体积。
这次优化让我深刻体会到,前端性能优化不是玄学,每一步都是有数据支撑的。打开DevTools,看Network、看Performance、看Lighthouse,哪里慢就盯哪里,对症下药,效果立竿见影。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
- 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现