前端vue性能优化
网络方面
优化资源
代码分割懒加载
1、路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
js
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [
{ path: '/users/:id', component: UserDetails }
// 或在路由定义里直接使用它
{ path: '/users/:id', component: () => import('./views/UserDetails.vue') },
],
})
component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。一般来说,对所有的路由都使用动态导入是个好主意。
注意
不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。
如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。
2.把组件按组分块
使用 webpack
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):
const UserDetails = () =>
import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
使用 Vite
在Vite中,你可以在rollupOptions下定义分块:
//vue.config.js
export default defineConfig({
build: {
rollupOptions: {
// https://rollupjs.org/guide/en/#outputmanualchunks
output: {
manualChunks: {
'group-user': [
'./src/UserDetails',
'./src/UserDashboard',
'./src/UserProfileEdit',
],
},
},
},
},
})
3.分割公共组件
不做处理的话,在打包后的dist里可以发现,公共组件的代码重复出现在各个js文件里。
处理方式是使用动态引入,并命名为一个单独的chunk
4.弹窗类需要被触发才显示的组件可以懒加载
这样可以进一步提升页面加载速度,减少白屏时间
合理使用 Tree shaking
Tree shaking 的作用:消除无用的 JS 代码,减少代码体积
举个🌰:
// util.js
export function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里
tree-shaking 原理:
依赖于ES6的模块特性,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础
静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 tree-shaking 成为可能
Tree shaking 并不是万能的
并不是说所有无用的代码都可以被消除,还是上面的代码,换个写法 tree-shaking 就失效了
// util.js
export default {
targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
},
deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
};
// 引入并使用
import util from '../util';
util.targetType(null)
同样的,项目中只使用了 targetType 方法,未使用 deepClone 方法,项目打包后,deepClone 方法还是被打包到项目里

究其原因,export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效
这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一
1.用依赖库时采用按需加载,不要全部引入。
2.自己编写工具时采用函数式编程,更好发挥树摇优化
内存方面
合理的降低内存占用,解决内存泄漏,可以提高应用稳定性。
调试内存占用
观察内存占用,可以用Performance API,可以用devtools memory。
查看js堆快照,要重点看三个指标:
-
Retained Size: 对象及其依赖对象的内存大小。
Delta发生变化的全部对象的个数。净增对象个数。Freed Size新对象释放出的内存空间。
Heap Snapshot中的Constructor
- (closure) 通过函数闭包对一组对象的引用计数
- (array、string、number、regexp) 不同对象类型的列表,Array,String,Number,RexExp的属性
- (已编译代码) 与已编译代码相关的任何内容。
- HTMLDivElement、HTMLAnchorElement、DocumentFragment DOM对象。
- Dep、Observer、VNode、Watcher、VueComponent 这些是vue特有的对象。
即使是一个小对象,都可能间接的hold了庞大的内存。从而导致自动GC程序不能处理掉这些被间接hold的内存。
vue是监测不到我们不用某些模块的,只有绑定在vue实例上的实例才会与组件一起销毁,没有绑定的一定要主动销毁。
在利用class Filter去搜索constructor,观察delta size为正数,freed size为0,说明模块有没有没有销毁。
选型前端第三方框架时,不妨先测试测试是否存在内存泄露。
lodash
moment
开发代码层面的优化
console.log导致的内存泄露
只有 devtools 打开时,console 打印才会引起内存泄漏的,如果不打开控制台,console 是不会引起内存变化的。稳妥起见,需要在生产环境时使用插件过滤掉 console 打印
// vue.config.js
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 返回一个将会被合并的对象
return {
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: false,
terserOptions: {
compress: {
drop_console: true
}}})
]}};
}}};
正确使用闭包
闭包:简单理解为函数的函数,最常见的场景为:函数作为一个函数的参数,或函数作为一个函数的返回值时。
- 闭包的作用是什么?
1延伸变量作用域范围,读取函数内部的变量
错误的写法:闭包所引用的变量在函数外部。因为开发者需要非常小心,否则稍有不慎就会造成内存泄露,我们总是希望可以通过 gc 自动回收,避免人为干涉
正确的写法:闭包引用的变量定义在函数中。这样随着外部引用的销毁,该闭包就会被 gc 自动回收 (推荐),无需人工干涉
变量如果定义在函数内部,当函数运行完成后,这些内部变量就会完成它的生命周期,能够被垃圾回收,没有必要做手动清除。
总结来说:应尽量避免定义全局的临时变量,而函数内部仅涉及到本级作用域的变量(不会被闭包保留引用的)无需手动清除。对于全局公共变量、以及闭包内引用变量来说,仅在变量确实使用完毕、值不再需要保留时,才需要手动清除变量的值。
对于浏览器来说,还需要考虑 DOM 元素的情况。如果某个变量(以及对象类型变量上的属性、元素)保留了对于 DOM 元素的引用,包括引用本页的元素或同源的 iframe 内的元素等情况,当该元素在页面上被删除时,对于它的引用很可能还被保留在变量内,这样就会造成内存泄露。
使用弱引用weakMap来处理监听事件的注册
// 代码1
element.addEventListener('click', handler, false)
// 代码2
weak.set(element, handler)
element.addEventListener('click', weak.get(element), false)
代码 2 比起代码 1 的好处是:由于监听函数是放在 WeakMap 里面,一旦 element 对象的其他引用消失,与它绑定的监听函数 handler 所占的内存也会被自动释放
UI渲染线程方面
骨架屏优化白屏时长
使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目
SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,需要通过加载 JS 将内容挂载到根节点上,这套机制的副作用:会造成长时间的白屏
常见的骨架屏插件就是基于这种原理,在项目打包时将骨架屏的内容直接放到 html 文件的根节点中
骨架屏插件
这里以 vue-skeleton-webpack-plugin 插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏
大数据列表虚拟滚动
首页中不乏有需要渲染长列表的场景,当渲染条数过多时,所需要的渲染时间会很长,滚动时还会造成页面卡顿,整体体验非常不好
虚拟滚动——指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的
虚拟滚动图例:

虚拟滚动基本原理:
计算出 totalHeight 列表总高度,并在触发时滚动事件时根据 scrollTop 值不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素
虚拟滚动插件
虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized 等
Web Worker 优化长任务
由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况
查看页面的长任务:
打开控制台,选择 Performance 工具,点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务(长任务:执行时间超过50ms的任务)

Web Worker 具体的使用与案例,详情见 一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥
Web Worker 的通信时长
并不是执行时间超过 50ms 的任务,就可以使用 Web Worker,还要先考虑通信时长的问题
假如一个运算执行时长为 100ms,但是通信时长为 300ms, 用了 Web Worker可能会更慢
比如新建一个 web worker, 浏览器会加载对应的 worker.js 资源,下图中的 Time 是这个资源的通信时长(也叫加载时长)

当任务的运算时长 - 通信时长 > 50ms,推荐使用Web Worker
Web Worker的限制
1、在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象
2、Worker中只能获取到部分浏览器提供的 API,如定时器、navigator、location、XMLHttpRequest等
3、由于可以获取XMLHttpRequest 对象,可以在 Worker 线程中执行ajax请求
4、每个线程运行在完全独立的环境中,需要通过postMessage、 message事件机制来实现的线程之间的通信
5.Worker 内部不能直接 import npm 包。如果需要在 Worker 里使用库(如 lodash 或 spark-md5):
- 下载库的
.min.js文件。 - 使用
self.importScripts('/path/to/spark-md5.min.js')引入
6.陷阱
| 陷阱类型 | 现象 | 最佳实践 |
|---|---|---|
| 过度使用 | 简单任务变慢 | 仅用于耗时 > 50ms 的计算密集型任务 |
| 大数据传输 | 内存翻倍、页面卡死 | 使用 Transferable Objects (ArrayBuffer) |
| 频繁通信 | UI 依然卡顿 | 批量聚合消息,减少 postMessage 次数 |
| 资源管理 | 内存泄漏、CPU 占用高 | 用完即焚 (terminate) 或使用线程池 |
| DOM 操作 | 代码报错 | Worker 中绝对禁止操作 DOM,只能通过消息通知主线程 |
后续更新中...
更多推荐
所有评论(0)