Vue3 高级性能优化
Vue3 高级性能优化
- 1. Lighthouse(谷歌浏览器性能分析工具)
- 2. 代码分析插件(rollup-plugin-visualizer)
- 3. Vite 配置优化配置
- 4. PWA 离线存储插件(vite-plugin-pwa)
- 5. 与 Vue 本身相关的优化
- 5.1 页面加载优化
- 5.1.1 包体积与 tree-shaking 优化
- 5.1.2 代码分割
- 5.1.3 选择合适的架构(暂时不做探究)
- 5.2 更新优化
- 5.2.1 Prop 稳定性(尽量让子组件的 prop 不发生改动)
- 5.2.2 v-once(元素和组件之渲染一次,跳过更新)
- 5.2.3 v-meno(有条件的跳过更新,3.2 以上版本可用)
- 5.2.4 计算属性稳定性(返回对象时对比具体属性,再决定是否改变)
- 5.3 通用优化
- 5.3.1 v-lazy(图片懒加载)
- 5.3.2 大型虚拟列表(只渲染视口部分)
- 5.3.3 减少大型不可变数据的响应性开销(使用 shallowRef() 和 shallowReactive())
- 5.3.4 避免不必要的组件抽象
- 6. 题外话
1. Lighthouse(谷歌浏览器性能分析工具)
Lighthouse 是谷歌浏览器中 devtools 中的一个性能分析工具。

选择Desktop(桌面端),点击 analyze page load,即可看到页面加载的一个跑分情况。

主要参数介绍:
(1)First Contentful Paint(首屏内容绘制,FCP):首次内容绘制的时间,浏览器第一次绘制DOM相关的内容,也是用户第一次看到页面内容的时间。
(2)Total Blocking Time(总阻塞时间,TBT):记录了首次内容绘制到用户可交互之间的时间,这段时间内,主进程被阻塞,会阻碍用户的交互,页面点击无反应。
(3)Speed Index(速度指数):页面各个可见部分的显示平均时间,当我们的页面上存在轮播图或者需要从后端获取内容加载时,这个数据会被影响到。
(4)Largest Contentful Paint(最大内容绘制):最大内容绘制时间,页面最大的元素绘制完成的时间。
(5)Cumulative Layout Shift(计算布局偏移):计算布局偏移值得分,会比较两次渲染帧的内容偏移情况,这个值通常为0。否则,可能出现用户想点击A按钮,但下一帧中,A按钮被挤到旁边,导致用户实际点击了B按钮。
2. 代码分析插件(rollup-plugin-visualizer)
(1)下载 代码分析插件:
npm i rollup-plugin-visualizer
(2)执行 npm run build ,会自动跳出代码分析页面。

可以看到,因为项目使用了完整引入 ELement Plus,所以项目打包内容十分庞大,在 1.53MB 左右。
据此,我们可以将 Element Plus 修改为按需引入,方式可参照《Vue3 组件库 Element Plus》的 2.2.2 自动按需引入 小节。

假设我们项目只使用了el-button组件,可以看到,重新npm run build后,element-plus 的打包体积大大减小,只剩下了 18.36KB。
3. Vite 配置优化配置
vite.config.js
build: {chunkSizeWarningLimit: 2000, // 警告单个文件大小限制,默认2000kbcssCodeSplit: true, //css 拆分sourcemap: false, //不生成sourcemapminify: false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。assetsInlineLimit: 5000 //小于该值 图片将打包成Base64 },

4. PWA 离线存储插件(vite-plugin-pwa)
(1)下载插件
npm i vite-plugin-pwa
(2)在 vite.config.js 中进行配置:
import { VitePWA } from 'vite-plugin-pwa'
VitePWA({workbox: {cacheId: "XIaoman",//缓存名称runtimeCaching: [{urlPattern: /.*\.js.*/, //缓存文件handler: "StaleWhileRevalidate", //重新验证时失效options: {cacheName: "XiaoMan-js", //缓存js,名称expiration: {maxEntries: 30, //缓存文件数量 LRU算法maxAgeSeconds: 30 * 24 * 60 * 60 //缓存有效期}}}]},
})
(3)执行 npm run build 进行打包,生成 sw.js 文件。

(4)全局下载 http-server:
npm i -g http-server
(5)进入打包后的 dist 文件夹,本地打开:
cd dist
http-server -p 8000


5. 与 Vue 本身相关的优化
Vue 性能优化主要是在两个方面:
(1)页面加载性能。首次访问时,应用展示出内容与达到可交互状态的速度。这通常会用 Google 所定义的一系列 Web 指标 (Web Vitals) 来进行衡量。比如First Contentful Paint(首屏内容绘制时间),详情参照 1. Lighthouse(谷歌浏览器性能分析工具) 小节;
(2)更新性能。应用响应用户输入更新的速度。比如当用户在搜索框中输入时结果列表的更新速度,或者用户在一个单页面应用 (SPA) 中点击链接跳转页面时的切换速度。
5.1 页面加载优化
5.1.1 包体积与 tree-shaking 优化
(1)按需引入。
采用现代化的打包工具,会支持自动化的tree-shaking(没用过的代码不会被打包)。所以最好许多函数库或者组件库采用按需引入的方式会更合适。
(2)避免引入过重依赖。
比如 lodash 默认是 commonjs 版本,对 tree-shaking 很不友好,所以可以使用 lodash-es (ES 版本)进行替代,支持tree-shaking。
5.1.2 代码分割
代码分割是指构建工具将构建后的 JavaScript 包拆分为多个较小的,可以按需或并行加载的文件。通过适当的代码分割,页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。
像 Rollup (Vite 就是基于它之上开发的) 或者 webpack 这样的打包工具可以通过分析 ESM 动态导入的语法来自动进行代码分割:
// lazy.js 及其依赖会被拆分到一个单独的文件中
// 并只在 `loadLazy()` 调用时才加载
function loadLazy() {return import('./lazy.js')
}
使用懒加载 + 异步组件
懒加载对于页面初次加载优化极大。可与 Vue 异步组件进行搭配使用,为组件创建分离的代码块:
import { defineAsyncComponent } from 'vue'// 会为 Foo.vue 及其依赖创建单独的一个块
// 它只会按需加载
// (即该异步组件在页面中被渲染时)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
5.1.3 选择合适的架构(暂时不做探究)
如果你的用例对页面加载性能很敏感,请避免将其部署为纯客户端的 SPA,而是让服务器直接发送包含用户想要查看的内容的 HTML 代码。纯客户端渲染存在首屏加载缓慢的问题,这可以通过服务器端渲染 (SSR) 或静态站点生成 (SSG) 来缓解。查看 SSR 指南以了解如何使用 Vue 实现 SSR。如果应用对交互性要求不高,你还可以使用传统的后端服务器来渲染 HTML,并在客户端使用 Vue 对其进行增强。
不过,我们目前的学习进程都是纯客户端的SPA,其他架构的内容暂时不做探究。
5.2 更新优化
5.2.1 Prop 稳定性(尽量让子组件的 prop 不发生改动)
子组件中任意一个 prop 更新,整个组件就会更新。看个例子:
<ListItemv-for="item in list":id="item.id":active-id="activeId" />
我们切换列表项的 activeId 时,所有的 ListItem 组件都发生了更新,明显不合理。修改为:
<ListItemv-for="item in list":id="item.id":active="item.id === activeId" />
将对比逻辑放入到父组件中实现,active 这个 prop 每次修改,只会更新两个子组件。
5.2.2 v-once(元素和组件之渲染一次,跳过更新)
v-once 仅渲染元素和组件一次,并跳过之后的更新。
在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 带有子元素的元素 -->
<div v-once><h1>Comment</h1><p>{{msg}}</p>
</div>
<!-- 组件 -->
<MyComponent v-once :comment="msg" />
<!-- `v-for` 指令 -->
<ul><li v-for="i in list" v-once>{{i}}</li>
</ul>
5.2.3 v-meno(有条件的跳过更新,3.2 以上版本可用)
v-meno 缓存一个模板的子树。用来有条件地跳过某些大型子树或者 v-for 列表的更新。
在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。比如:
<div v-memo="[valueA, valueB]">...
</div>
如果 v-memo="[]",效果就和 v-once 一致。
5.2.4 计算属性稳定性(返回对象时对比具体属性,再决定是否改变)
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)watchEffect(() => console.log(isEven.value)) // true// 这将不会触发新的输出,因为计算属性的值依然为 `true`
count.value = 2
count.value = 4
上面代码中,只有 isEven 计算属性的返回值发生改变,才会导致 watchEffect 副作用的发生。
但是,如果前一个计算属性返回的是对象,则每次产生的都是一个新的引用值,会导致 watchEffect 副作用每次都会发生:
const computedObj = computed(() => {return {isEven: count.value % 2 === 0}
})
但是我们可以通过手动对比新旧属性值的方式进行优化:
const computedObj = computed((oldValue) => {const newValue = {isEven: count.value % 2 === 0}if (oldValue && oldValue.isEven === newValue.isEven) {return oldValue}return newValue
})
5.3 通用优化
5.3.1 v-lazy(图片懒加载)
<img v-lazy="imgUrl" >
5.3.2 大型虚拟列表(只渲染视口部分)
成千上万个列表项的列表会需要大量的 DOM,渲染全部会导致页面卡顿。
在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。
可使用的库:
(1)vue-virtual-scroller
(2)vue-virtual-scroll-grid
(3)vueuc/VVirtualList
(4)Element Plus 的 Virtualized Table 虚拟化表格、Tree V2 虚拟化树形控件 和 Virtualized Select 虚拟化选择器
5.3.3 减少大型不可变数据的响应性开销(使用 shallowRef() 和 shallowReactive())
使用 shallowRef() 和 shallowReactive() 来绕开深度响应,将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新:
const shallowArray = shallowRef([/* 巨大的列表,里面包含深层的对象 */
])// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [{...shallowArray.value[0],foo: 1},...shallowArray.value.slice(1)
]
5.3.4 避免不必要的组件抽象
有些时候我们会去创建无渲染组件或高阶组件 (用来渲染具有额外 props 的其他组件) 来实现更好的抽象或代码组织。虽然这并没有什么问题,但请记住,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。
需要提醒的是,只减少几个组件实例对于性能不会有明显的改善,所以如果一个用于抽象的组件在应用中只会渲染几次,就不用操心去优化它了。考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。
6. 题外话
当然,性能优化是一个很大的话题,远远不是我们这篇文章所能够一下概括的。如果大家感兴趣,并且我后续有时间的话,可以专门就这一话题,再重新开一个专栏,和大家一起学习,深入探讨性能优化的各种基础知识和可能性。
如果大家有其他优化经验或者建议的话,也欢迎在评论区发表意见,共同学习😄
上一章 《webpack 从零构建 Vue3》
