Vue 性能优化策略
文章目录
- 前言
- 一、优化原则
- 1.1 先测量,再优化
- 1.2 优化层次
- 二、渲染层优化
- 2.1 合理使用 key
- 2.2 v-once:静态内容只渲染一次
- 2.3 v-memo:条件缓存子树(Vue 3.2+)
- 2.4 v-memo vs computed
- 2.5 v-if vs v-show
- 2.6 减少组件嵌套与无效 render
- 三、响应式层优化
- 3.1 shallowRef / shallowReactive
- 3.2 markRaw:标记永不代理
- 3.3 减少 reactive 范围
- 3.4 computed 缓存 vs watch
- 四、代码与加载优化
- 4.1 路由懒加载
- 4.2 组件异步加载
- 4.3 Tree-shaking
- 4.4 KeepAlive 缓存页面
- 五、大列表:虚拟滚动
- 5.1 问题
- 5.2 方案
- 5.3 配合优化
- 六、其他实用手段
- 6.1 防抖 / 节流
- 6.2 Web Worker
- 6.3 图片与资源
- 6.4 编译层(框架内置)
- 七、面试聚焦
- 7.1 优化应基于实际测量
- 7.2 shallowRef 的 .value 变化会触发更新吗?
- 7.3 v-memo 和 computed 的区别?
- 7.4 v-once 和静态提升的区别?
- 八、易混淆点
- 九、思考与练习
- 总结
前言
Vue 3 本身已有编译优化和 Proxy 响应式,但业务场景仍可能出现大列表、频繁更新、首屏过慢等问题。本篇从渲染、数据、代码、测量四个层面梳理 Vue 性能优化策略。本篇会讲清楚:
- shallowRef / shallowReactive / markRaw
- v-once / v-memo 的使用与区别
- 大列表虚拟滚动与懒加载
- 优化应基于实际测量
一、优化原则
1.1 先测量,再优化
// 不要凭感觉优化// 1. Chrome Performance 面板 → 录制交互,看 Long Task、Layout、Paint// 2. Lighthouse → FCP、LCP、TBT// 3. Vue DevTools → 组件 render 次数、耗时| 原则 | 说明 |
|---|---|
| 有数据再动手 | 定位瓶颈是 render、网络还是计算 |
| 避免过早优化 | 简单页面不必堆 v-memo、shallowRef |
| 优先低成本 | 懒加载、合理 key 往往比改架构见效快 |
| 关注用户感知 | LCP、INP 比纯 JS 执行时间更重要 |
1.2 优化层次
编译层(Vue 3 内置)→ 静态提升、PatchFlag、Block Tree 渲染层 → key、v-once、v-memo、虚拟滚动 响应式层 → shallowRef、markRaw、减少 reactive 范围 代码层 → 路由/组件懒加载、Tree-shaking二、渲染层优化
2.1 合理使用 key
列表用稳定唯一 id作 key,减少错误复用和不必要 DOM 操作(详见《Key 的作用与原理》)。
2.2 v-once:静态内容只渲染一次
<header v-once> <h1>{{ title }}</h1> <nav>固定导航</nav> </header>- 首次渲染后跳过该子树后续所有更新(含 props 变化)
- 适合页脚、版权、固定说明等几乎不变的内容
- 与编译期静态提升不同:v-once 是运行时指令,开发者手动标记
2.3 v-memo:条件缓存子树(Vue 3.2+)
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]"> <ExpensiveItem :item="item" /> </div>- 依赖数组不变→ 跳过该子树更新(含子组件)
- 依赖变化→ 正常更新
- 适合大列表中昂贵子组件,且只有部分字段影响展示
<!-- 选中态变化才重渲染该项 --> <div v-memo="[item.id, item.selected]"> <ItemCard :item="item" /> </div>2.4 v-memo vs computed
| 对比项 | v-memo | computed |
|---|---|---|
| 作用位置 | 模板子树 | script 逻辑 |
| 跳过 | 整段模板 + 子组件 render | 重复计算 |
| 依赖 | 显式数组[a, b] | 自动收集 |
v-memo 管「这块模板要不要重绘」;computed 管「这个值要不要重算」。
2.5 v-if vs v-show
| 场景 | 推荐 |
|---|---|
| 频繁切换显示 | v-show(只改 display) |
| 很少出现 | v-if(不渲染 DOM) |
2.6 减少组件嵌套与无效 render
- 不要把大对象整包 reactive 后传给大量子组件
- 用
v-memo、拆分组件、Pinia 按需订阅减少无关更新
三、响应式层优化
3.1 shallowRef / shallowReactive
import{shallowRef,shallowReactive,triggerRef}from'vue'// 只代理 .value 引用变化,不深度代理内部constbigData=shallowRef({list:[...10000items]})// ✅ 替换整个对象 → 触发更新bigData.value={list:newList}// ❌ 改深层属性 → 不触发更新bigData.value.list.push(item)// 手动触发bigData.value.list.push(item)triggerRef(bigData)| API | 深度代理 | 触发更新 |
|---|---|---|
ref | 是(对象) | 深层变化也触发 |
shallowRef | 否 | 仅.value替换触发 |
reactive | 是 | 深层变化触发 |
shallowReactive | 仅第一层 | 仅第一层属性触发 |
场景:第三方库实例、大型不可变数据、实时 tick 数据批量替换。
3.2 markRaw:标记永不代理
import{markRaw,reactive}from'vue'constchart=markRaw(echarts.init(dom))conststate=reactive({chart,// 不会被 proxy,避免无效依赖option:{}})适合 ECharts、地图 SDK 等不需要响应式的大对象。
3.3 减少 reactive 范围
// ❌ 整个表单大对象都 reactiveconstform=reactive({/* 50 个字段 */})// ✅ 只有会驱动视图的字段 reactiveconstvisibleFields=reactive({name:'',status:''})conststaticConfig={/* 只读配置,普通对象 */}3.4 computed 缓存 vs watch
// ✅ 派生数据用 computed,有缓存constfiltered=computed(()=>list.value.filter(i=>i.active))// ✅ watch 默认懒执行,避免 immediate 滥用watch(source,handler)// 默认 flush: 'pre',按需触发四、代码与加载优化
4.1 路由懒加载
{path:'/report',component:()=>import('@/views/Report.vue')}按路由拆 chunk,减小首屏 JS(详见《路由懒加载》)。
4.2 组件异步加载
constHeavyChart=defineAsyncComponent(()=>import('./HeavyChart.vue'))弹窗、Tab 内重型组件按需加载(详见《动态组件与异步组件》)。
4.3 Tree-shaking
- 按需导入:
import { ref } from 'vue',避免全量 - 工具库:
import debounce from 'lodash-es/debounce' - 构建工具生产模式自动 tree-shake
4.4 KeepAlive 缓存页面
Tab、列表→详情→返回场景缓存实例,避免重复创建(详见《KeepAlive 缓存组件》)。注意max控制内存。
五、大列表:虚拟滚动
5.1 问题
渲染 1 万条<li>→ 1 万个 DOM 节点 → 卡顿、内存高。
5.2 方案
只渲染可视区域+ 少量缓冲区的项:
<script setup> import { RecycleScroller } from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' </script> <template> <RecycleScroller :items="list" :item-size="50" key-field="id" v-slot="{ item }" > <div class="row">{{ item.name }}</div> </RecycleScroller> </template>常用库:vue-virtual-scroller、@tanstack/vue-virtual。
5.3 配合优化
- 列表项用
v-memo="[item.id, item.xxx]" - 项内避免深层 reactive
- 稳定
key用业务 id
六、其他实用手段
6.1 防抖 / 节流
搜索输入、scroll、resize 监听使用 debounce/throttle,减少 render 和请求次数。
6.2 Web Worker
大 JSON 解析、复杂计算放 Worker,避免阻塞主线程 UI。
6.3 图片与资源
- 懒加载:
loading="lazy"、v-lazy 指令 - 合适格式:WebP、压缩、CDN
- 路由 prefetch 空闲预加载(详见《路由懒加载》)
6.4 编译层(框架内置)
Vue 3 模板自动静态提升、PatchFlag、Block Tree,无需手写(详见《编译优化》)。手写 render 函数享受不到,优先写模板。
七、面试聚焦
7.1 优化应基于实际测量
Performance、Lighthouse、Vue DevTools 定位瓶颈,避免盲目 v-memo。
7.2 shallowRef 的 .value 变化会触发更新吗?
会。shallowRef.value = newObj触发更新;改value内部深层属性不会,需triggerRef或整体替换。
7.3 v-memo 和 computed 的区别?
v-memo 跳过模板子树 render;computed 缓存计算结果。一个管视图,一个管逻辑。
7.4 v-once 和静态提升的区别?
静态提升是编译期自动识别静态节点;v-once 是运行时指令,手动标记且跳过后续所有更新。
八、易混淆点
- shallowRef 不是不更新:换
.value会更新,只是不深追踪内部。 - v-once 后 props 变也不更新:确认内容真正静态再用。
- v-memo 依赖要写全:漏写依赖会导致该更新不更新。
- 虚拟滚动不减少数据量:只减少 DOM 数量,接口仍可能要分页。
- KeepAlive 占内存:需
max和include控制。
九、思考与练习
1.大列表卡顿如何优化?
解析:虚拟滚动减少 DOM;稳定 key;v-memo 昂贵子项;分页或懒加载数据。
2.shallowRef 适用场景?
解析:大型对象整体替换、第三方实例、频繁替换 .value 但不关心深层变化的场景。
3.v-memo 依赖数组如何写?
解析:列出影响该子树展示的所有响应式值,如[item.id, item.status]。
4.首屏 JS 过大怎么办?
解析:路由懒加载、异步组件、Tree-shaking、分析 bundle(rollup-plugin-visualizer)。
5.为什么 say「先测量再优化」?
解析:避免无效优化增加复杂度;不同瓶颈方案不同(网络 vs render vs 计算)。
总结
- 原则:先测量,再针对性优化,避免过早优化
- 渲染:key、v-once、v-memo、v-show/v-if、虚拟滚动
- 响应式:shallowRef、markRaw、缩小 reactive 范围、computed 缓存
- 代码:路由/组件懒加载、Tree-shaking、KeepAlive
- 本质:减少 DOM 数量、减少无效 render、减小首屏体积
