第 26 章:性能优化
学习目标
- 用
v-memo/shallowRef减少渲染 - 用
defineAsyncComponent异步加载 - 用
markRaw标记非响应式 - 配合打包工具分析
一、v-memo 跳过渲染
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.name]">
{{ item.name }}
</div>
</template>只有 item.id 或 item.name 变化时,才重渲该项。
⚠️ 坑 1:
v-memo不能代替:key,二者必须同时存在。
💡
v-memo= 白名单渲染:数组里的字段变了才重渲,其它字段变了直接跳过——只用于大列表性能优化。
二、shallowRef / shallowReactive
import { shallowRef, triggerRef } from 'vue'
const bigList = shallowRef<Item[]>([])
bigList.value = await fetchList() // 只跟踪引用变化
triggerRef(bigList) // 手动触发适合大列表、第三方实例。
💡
shallowRef= 只监听.value换,不监听内部;大列表 / 第三方实例必用;triggerRef是内部改了不响应的"手动刷新"按钮。
三、defineAsyncComponent
import { defineAsyncComponent } from 'vue'
const Heavy = defineAsyncComponent(() => import('./Heavy.vue'))<template>
<Heavy v-if="showHeavy" />
</template>配合 Webpack /* webpackPrefetch: true */ 提前预加载。
完整配置项:
const Heavy = defineAsyncComponent({
loader: () => import('./Heavy.vue'), // ① 加载函数
loadingComponent: Loading, // ② 加载中显示的组件(可选)
errorComponent: ErrorView, // ③ 加载失败显示的组件(可选)
delay: 200, // ④ 200ms 内不显示 loading(避免闪屏)
timeout: 5000, // ⑤ 超时 5s 没下完就报错
})| 配置 | 干啥 |
|---|---|
loadingComponent | 网络慢时给个 loading 占位 |
errorComponent | 加载失败显示 |
delay | 200ms 内不显示 loading,避免一闪而过 |
timeout | 超时时间 |
💡
defineAsyncComponent= 把组件包成"按需下载"——首屏不下载,用到再下;Vue Router 内部其实就是用它包装的。"用到再下"的两层机制:
v-if控制"用不用",包装组件拦截"何时调 loader"。三步时序:
1️⃣ showHeavy = false → <Heavy> 不在渲染树 → 包装组件不挂载 → loader() 不调用 → Heavy.vue 不下载 2️⃣ showHeavy = true → Vue 把 <Heavy> 渲染到 DOM → 包装组件首次挂载 → loader() 被调用 → import 下载 → 显示 loadingComponent → 下载完 → 渲染真正的 Heavy.vue 内容 3️⃣ showHeavy = false → 再 true → 包装组件又挂载 → loader() 再次调用? → ❌ **不会!** 内部有缓存,直接复用没
v-if会怎样:<Heavy />始终在 DOM,首次渲染就触发 loader,首屏就下载了,失去按需加载的意义——所以v-if是关键。
四、markRaw 标记
import { markRaw } from 'vue'
import Monaco from 'monaco-editor'
const editor = markRaw(new Monaco()) // 不做响应式代理💡 裸
new Monaco()单独没事,但只要塞进ref/reactive就会被 Vue 包 Proxy 改坏;markRaw是"别动我"的贴标签——所有第三方实例都建议 markRaw,稳。
五、v-once / v-show
<p v-once>永不变化的静态内容</p>
<p v-show="ok">频繁切换用 v-show</p>六、路由懒加载
{ path: '/admin', component: () => import('@/views/Admin.vue') }七、虚拟列表
npm i vue-virtual-scroller<RecycleScroller :items="bigList" :item-size="50" key-field="id">
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>💡 虚拟列表 = 10 万条只渲染屏幕可见的十几条 DOM,外层用 spacer 假装它们都在——滚动时复用这几个 DOM 改内容,大列表不卡的唯一办法。
核心机制:
可视区(只有 12 个真实 <li>) ┌────────────┐ │ item1~12 │ ← 真实 DOM └────────────┘ 外层高度 = 100000 × 50 = 5000000px(假装 10 万条都在)滚动:
item1滚出 → 不销毁,改成显示item13。永远是这 12 个 DOM 在循环,所以叫 "RecycleScroller"。三件套:
参数 作用 :items数据源 :item-size每行高度(必填,必须固定) key-field唯一 key 啥时候用:
数据量 用啥 < 1000 条 普通 v-for > 1000 条(尤其 1 万+) 虚拟列表必用 一句话:10 万条列表不卡的唯一办法——只渲染屏幕可见的十几条,外层撑高假装都在,滚动时复用 DOM 改内容。
八、分包策略
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
},
},
},
}九、分析工具
npm i -D rollup-plugin-visualizerimport { visualizer } from 'rollup-plugin-visualizer'
plugins: [visualizer({ open: true })]💡
rollup-plugin-visualizer= 包体积分析器——打包完后自动生成 HTML 报告(彩色方块图),一眼看出哪个依赖占空间,做首屏优化的第一步就是跑它看报告。三步使用:
bash# ① 装包 npm i -D rollup-plugin-visualizer # ② vite.config.ts 加插件 # ③ 跑 build → 自动打开 stats.html npm run build报告长这样(方块越大 = 越占空间):
┌─────────────────────────────────────────┐ │ index.html │ ├─────────────┬───────────────────────────┤ │ src/main │ 第三方依赖大块 │ │ src/views │ ┌──────────────┐ │ │ ... │ │ element-plus │ ← 占40% │ ├─────────────┤ ├──────────────┤ │ │ vue │ │ echarts │ ← 占 25%│ │ pinia │ ├──────────────┤ │ │ ... │ │ vue-router │ ← 占 8% │ └─────────────┴───┴──────────────┴─────────┘ ↑ 你的代码小 ↑ 这些是占空间的大头看报告 → 优化对照:
看报告发现 怎么优化 element-plus占 70%按需导入( unplugin-vue-components)整个 echarts引进来 500KB用 echarts/core按需引入moment占了 200KB换成 dayjs(轻 97%)某个 .png占了 3MB转 webp / 压缩 业务代码 5MB 加 manualChunks(19 章三)几个常用配置:
tsvisualizer({ open: true, // 构建完自动打开报告 filename: 'stats.html', // 输出文件名 gzipSize: true, // 显示 gzip 后的大小(更接近真实传输体积) brotliSize: true, // 显示 brotli 后的大小 template: 'treemap', // treemap / sunburst / network })
十、避免大响应式
const data = ref({ big: hugeArray })
// ❌ 深响应,巨大性能开销
data.value.big.push(...)
// ✅ 用 shallowRef + replace
const data = shallowRef({ big: [] })
data.value = { big: [...hugeArray] }⚠️ 坑 2:Pinia store 不要塞超大数组,改用
markRaw+ 单组件 ref。
十一、防抖节流
const onScroll = useDebounceFn(() => fetchData(), 200)十二、本章小结
| 性能点 | 方案 |
|---|---|
| 列表渲染 | v-memo / 虚拟列表 |
| 大数据 | shallowRef |
| 异步组件 | defineAsyncComponent |
| 路由 | 懒加载 |
| 打包 | manualChunks |
动手练习
- 大列表:用虚拟列表渲染 1 万行
- 路由分包:按需加载 admin 模块
- v-memo:在长列表中跳过未改项
下一章:第 27 章:测试 →