Vue 性能优化实战
1 常见的性能优化分析方式;
用于生产部署的负载性能分析:
- PageSpeed Insights
- WebPageTest
用于本地开发期间的性能分析:
- Chrome 开发者工具“性能”面板
- app.config.performance 将会开启 Vue 特有的性能标记,标记在 Chrome 开发者工具的性能时间线上。
- Vue 开发者扩展也提供了性能分析的功能。
2 页面加载优化;
2.1 选用正确的架构
如果对页面加载性能很敏感,请避免将其部署为纯客户端的 SPA,而是让服务器直接发送包含用户想要查看的内容的 HTML 代码。纯客户端渲染存在首屏加载缓慢的问题,这可以通过服务器端渲染 (SSR)
或静态站点生成 (SSG)
来缓解。如果应用对交互性要求不高,你还可以使用传统的后端服务器来渲染 HTML,并在客户端使用 Vue 对其进行增强。
如果你的主应用必须是 SPA,但还有其他的营销相关页面 (落地页、关于页、博客等),请单独部署这些页面!理想情况下,营销页面应该是包含尽可能少 JS 的静态 HTML,并用 SSG
方式部署。
2.2 包体积与 Tree-shaking 优化
一个最有效的提升页面加载速度的方法就是压缩 JavaScript 打包产物的体积。当使用 Vue 时有下面一些办法来减小打包产物体积:
- 尽可能地采用构建步骤
- 如果使用的是相对现代的打包工具,许多 Vue 的 API 都是可以被
tree-shake
的。举例来说,如果你根本没有使用到内置的 组件,它将不会被打包进入最终的产物里。Tree-shaking 也可以移除你源代码中其他未使用到的模块。 - 当使用了构建步骤时,模板会被预编译,因此我们无须在浏览器中载入 Vue 编译器。这在同样最小化加上 gzip 优化下会相对缩小 14kb 并避免运行时的编译开销。
- 如果使用的是相对现代的打包工具,许多 Vue 的 API 都是可以被
- 在引入新的依赖项时要小心包体积膨胀!在现实的应用中,包体积膨胀通常因为无意识地引入了过重的依赖导致的。
- 如果使用了构建步骤,应当尽量选择提供 ES 模块格式的依赖,它们对 tree-shaking 更友好。举例来说,选择 lodash-es 比 lodash 更好。
- 查看依赖的体积,并评估与其所提供的功能之间的性价比。如果依赖对 tree-shaking 友好,实际增加的体积大小将取决于你从它之中导入的 API。像
bundlejs.com
这样的工具可以用来做快速的检查,但是根据实际的构建设置来评估总是最准确的。
2.3 代码分割
代码分割是指构建工具将构建后的 JavaScript 包拆分为多个较小的,可以按需或并行加载的文件。通过适当的代码分割,页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。
像 Rollup (Vite 就是基于它之上开发的) 或者 webpack 这样的打包工具可以通过分析 ESM 动态导入的语法来自动进行代码分割:
// lazy.js 及其依赖会被拆分到一个单独的文件中
// 并只在 `loadLazy()` 调用时才加载
function loadLazy() {
return import('./lazy.js')
}
懒加载对于页面初次加载时的优化帮助极大,它帮助应用暂时略过了那些不是立即需要的功能。在 Vue 应用中,这可以与 Vue 的异步组件
搭配使用,为组件树创建分离的代码块:
import { defineAsyncComponent } from 'vue'
// 会为 Foo.vue 及其依赖创建单独的一个块
// 它只会按需加载
//(即该异步组件在页面中被渲染时)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
3 页面更新优化;
3.1 Props 稳定性
在 Vue 之中,一个子组件只会在其至少一个 props 改变时才会更新。思考以下示例:
<ListItem
v-for="item in list"
:id="item.id"
:active-id="activeId" />
在 <ListItem>
组件中,它使用了 id
和 activeId
两个 props
来确定它是否是当前活跃的那一项。虽然这是可行的,但问题是每当 activeId
更新时,列表中的每一个 <ListItem>
都会跟着更新!
理想情况下,只有活跃状态发生改变的项才应该更新。我们可以将活跃状态比对的逻辑移入父组件来实现这一点,然后让 <ListItem>
改为接收一个 active prop
:
<ListItem
v-for="item in list"
:id="item.id"
:active="item.id === activeId" />
3.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>
3.3 v-memo
v-memo
是一个内置指令,可以用来有条件地跳过某些大型子树或者 v-for
列表的更新。
缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。举例来说:
<div v-memo="[valueA, valueB]">
...
</div>
当组件重新渲染,如果 valueA
和 valueB
都保持不变,这个 <div>
及其子项的所有更新都将被跳过。实际上,甚至虚拟 DOM
的 vnode
创建也将被跳过,因为缓存的子树副本可以被重新使用。
正确指定缓存数组很重要,否则应该生效的更新可能被跳过。v-memo
传入空依赖数组 (v-memo="[]")
将与 v-once
效果相同。
与 v-for
一起使用
v-memo
仅用于性能至上场景中的微小优化,应该很少需要。最常见的情况可能是有助于渲染海量 v-for 列表 (长度超过 1000 的情况):
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
当组件的 selected
状态改变,默认会重新创建大量的 vnode
,尽管绝大部分都跟之前是一模一样的。v-memo
用在这里本质上是在说“只有当该项的被选中状态改变时才需要更新”。这使得每个选中状态没有变的项能完全重用之前的 vnode 并跳过差异比较。
4 通用优化;
- 虚拟滚动(根据可视区的高度来决定页面展示多少个item)
- 减少大型不可变数据的响应性开销(通过使用
shallowRef()
和shallowReactive()
来绕开深度响应) - 避免不必要的组件抽象(组件实例比普通 DOM 节点要昂贵得多)