Skip to content
第 26 章 前端 ⏱ 13 分钟阅读

第 26 章:性能优化 ​

学习目标 ​

  • 用 v-memo / shallowRef 减少渲染
  • 用 defineAsyncComponent 异步加载
  • 用 markRaw 标记非响应式
  • 配合打包工具分析

一、v-memo 跳过渲染 ​

vue
<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 ​

ts
import { shallowRef, triggerRef } from 'vue'

const bigList = shallowRef<Item[]>([])
bigList.value = await fetchList()      // 只跟踪引用变化
triggerRef(bigList)                    // 手动触发

适合大列表、第三方实例。

💡 shallowRef = 只监听 .value 换,不监听内部;大列表 / 第三方实例必用;triggerRef 是内部改了不响应的"手动刷新"按钮。

三、defineAsyncComponent ​

ts
import { defineAsyncComponent } from 'vue'

const Heavy = defineAsyncComponent(() => import('./Heavy.vue'))
vue
<template>
  <Heavy v-if="showHeavy" />
</template>

配合 Webpack /* webpackPrefetch: true */ 提前预加载。

完整配置项:

ts
const Heavy = defineAsyncComponent({
  loader: () => import('./Heavy.vue'),   // ① 加载函数
  loadingComponent: Loading,               // ② 加载中显示的组件(可选)
  errorComponent: ErrorView,               // ③ 加载失败显示的组件(可选)
  delay: 200,                              // ④ 200ms 内不显示 loading(避免闪屏)
  timeout: 5000,                           // ⑤ 超时 5s 没下完就报错
})
配置干啥
loadingComponent网络慢时给个 loading 占位
errorComponent加载失败显示
delay200ms 内不显示 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 标记 ​

ts
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 ​

vue
<p v-once>永不变化的静态内容</p>
<p v-show="ok">频繁切换用 v-show</p>

六、路由懒加载 ​

ts
{ path: '/admin', component: () => import('@/views/Admin.vue') }

七、虚拟列表 ​

bash
npm i vue-virtual-scroller
vue
<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 改内容。

八、分包策略 ​

ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vue: ['vue', 'vue-router', 'pinia'],
        ui: ['element-plus'],
      },
    },
  },
}

九、分析工具 ​

bash
npm i -D rollup-plugin-visualizer
ts
import { 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 章三)

几个常用配置:

ts
visualizer({
  open: true,                          // 构建完自动打开报告
  filename: 'stats.html',              // 输出文件名
  gzipSize: true,                      // 显示 gzip 后的大小(更接近真实传输体积)
  brotliSize: true,                    // 显示 brotli 后的大小
  template: 'treemap',                 // treemap / sunburst / network
})

十、避免大响应式 ​

ts
const data = ref({ big: hugeArray })

// ❌ 深响应,巨大性能开销
data.value.big.push(...)

// ✅ 用 shallowRef + replace
const data = shallowRef({ big: [] })
data.value = { big: [...hugeArray] }

⚠️ 坑 2:Pinia store 不要塞超大数组,改用 markRaw + 单组件 ref。

十一、防抖节流 ​

ts
const onScroll = useDebounceFn(() => fetchData(), 200)

十二、本章小结 ​

性能点方案
列表渲染v-memo / 虚拟列表
大数据shallowRef
异步组件defineAsyncComponent
路由懒加载
打包manualChunks

动手练习 ​

  1. 大列表:用虚拟列表渲染 1 万行
  2. 路由分包:按需加载 admin 模块
  3. v-memo:在长列表中跳过未改项

下一章:第 27 章:测试 →

本站基于 VitePress 构建 · 由 StackHub 团队维护