包体分析与按需加载
Vue 性能优化 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
首屏体积决定用户多久能看到内容。这一节掌握 Vite 的产物分析方法、路由级与组件级代码分割、依赖的按需引入,以及一个可执行的「包体预算」流程。
问题场景
项目上线后首屏加载 3 秒,Network 面板里一个 1.2MB 的 JS 文件。打开分析图:图表库、富文本编辑器、日期库全被打进主包——它们只在后台某个页面用一次。Vite 默认按动态 import 切块,但「静态 import 的第三方库」和「路由表里直接 import 的页面组件」都会进主包。
核心解释
先测量。Vite 构建产物默认输出 chunk 大小;更直观的方式是接可视化分析:
npm i -D rollup-plugin-visualizer
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [vue(), visualizer({ open: true, gzipSize: true, filename: "dist/stats.html" })],
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ["vue", "vue-router", "pinia"],
},
},
},
},
});
manualChunks 把稳定的框架依赖单独成块:业务代码更新时,框架块命中浏览器缓存,用户只需重新下载业务块。这是最常见的缓存优化。
路由级分割:路由表里用动态 import(V10 已用到),每条路由一个 chunk,例如 { path: "/", component: () => import("../views/HomeView.vue") }。首页路由如果想更快,也可以首屏同步 import、次级页面懒加载。
组件级分割:重组件(编辑器、图表、地图)用 defineAsyncComponent 按需加载,并配 loading 与 error 状态:
<script setup>
import { defineAsyncComponent } from "vue";
const RichEditor = defineAsyncComponent({
loader: () => import("./RichEditor.vue"),
loadingComponent: () => import("./EditorSkeleton.vue"),
delay: 200, // 200ms 内加载完成就不显示 loading
timeout: 10000,
});
</script>
<template>
<Suspense>
<RichEditor />
</Suspense>
</template>
依赖按需引入:UI 库、图标库最容易犯「全量引入」的错。以 lodash 为例,import _ from "lodash" 打包整个库;import debounce from "lodash/debounce" 只打一个函数。ECharts、Ant Design Vue 这类库都有按需引入的官方方案(组件级注册、图表按需注册)。
包体预算:给主包设一个上限(比如 gzip 后 200KB),CI 里用 bundlesize 或脚本检查 dist 体积,超了就让构建失败。没有预算的优化会慢慢退化。
示例拆解
一个真实的优化流程,按「测量 → 定位 → 改 → 复测」走:
# 1. 构建并查看产物
npm run build
# 输出示例:
# dist/assets/index-abc.js 620.41 kB │ gzip: 198.02 kB
# dist/assets/Admin-xyz.js 412.10 kB │ gzip: 131.20 kB
# 2. 打开 stats.html 找最大依赖(通常是图表/编辑器/日期库)
# 3. 改造:路由懒加载 + 重组件异步 + 依赖按需
# 4. 复测
npm run build
# dist/assets/index-def.js 212.07 kB │ gzip: 72.10 kB ← 主包下降 60%
验证「懒加载真的生效」:Network 面板里首屏只加载主包,进入后台页时才出现 Admin-*.js。如果首屏仍然加载了后台的块,检查是不是在别处静态 import 了同一个模块(比如共享工具文件里 import 了后台页面的常量),静态依赖会把整条链拉进主包。
动手任务
- 给一个项目接上 visualizer,找出体积最大的三个依赖,记录 gzip 后大小。
- 把其中最重的一个组件改成
defineAsyncComponent,验证首屏不再加载它。 - 设置一个包体检查脚本(读
dist/assets/*.js的总 gzip 大小,超过阈值退出码非 0),接进npm run build之后。
常见错误
- 全部依赖塞进 manualChunks 的一个块:任何依赖更新都让整块失效,缓存全废。框架与业务分开即可。
- 异步组件没有 loading:加载期间空白,用户以为卡死。配 loadingComponent 或 Suspense fallback。
- 动态 import 写在函数里但模块被静态引用:静态 import 优先,分割失效。检查 import 链。
- 只看未压缩体积:gzip/brotli 后的体积才接近用户实际下载量。
检查与复盘
- 能用 visualizer 定位主包里最大的依赖
- 能区分路由级与组件级分割的适用场景
- 能写出一个包体预算检查并解释为什么需要它
参考资料
- Vite 官方文档:构建选项与产物分析
- Vue 官方文档「异步组件」:defineAsyncComponent 配置
- Vue 官方文档「性能优化」:代码分割建议
读完这一课了?
标记完成后,工作台会安排接下来的学习。