今日工作总结:Vue 3 项目性能优化实践
📊 优化历程概览
1. 初始状态 - 默认配置
状态:一个H5页面,十几个模块同时导入、同时渲染
问题:首屏加载所有代码,资源体积大,初始加载慢
2. 优化第一步 - Vite分包
// vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
ui: ['element-plus'],
charts: ['echarts']
}
}
}
}
效果:
- ✅ 代码分割,减小单个文件体积
- ❌ 依然同时加载所有chunk
- ❌ 多个HTTP请求并发,可能阻塞
3. 优化第二步 - 异步组件
// 使用defineAsyncComponent
const AsyncChart = defineAsyncComponent(() =>
import('./components/Chart.vue')
)
效果:
- ✅ 按需加载,减少初始包大小
- ❌ Vue默认会预加载所有异步组件
- ❌ 进入页面时依然发起多个请求
4. 优化第三步 - 视口检测 + 懒加载(核心优化)
解决方案:结合 IntersectionObserver+ 异步组件
<!-- LazyContainer.vue 容器组件 -->
<template>
<div ref="containerRef">
<slot v-if="shouldLoad" />
</div>
</template>
<script setup>
// 关键:只有进入视口才渲染
const shouldLoad = ref(false)
// IntersectionObserver 监听
</script>
使用方式:
<template>
<!-- 模块1:首屏显示 -->
<Section1 />
<!-- 模块2:滚动后才加载 -->
<LazyContainer>
<AsyncSection2 />
</LazyContainer>
</template>
<script setup>
import Section1 from './Section1.vue'
import { defineAsyncComponent } from 'vue'
// 异步组件
const AsyncSection2 = defineAsyncComponent(
() => import('./Section2.vue')
)
</script>
分包策略配合:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
// 按模块分包
manualChunks(id) {
if (id.includes('Section1')) return 'section1'
if (id.includes('Section2')) return 'section2'
if (id.includes('Section3')) return 'section3'
}
}
}
}
}
5. 遇到的问题 - 动态导入警告
警告:The above dynamic import cannot be analyzed by Vite.
原因:Vite需要在构建时静态分析import路径
解决方案:
// ❌ 错误:动态路径(Vite无法分析)
const module = await import(`./${name}.vue`)
// ✅ 正确:静态路径
const AsyncComponent = defineAsyncComponent(() =>
import('./components/Chart.vue') // 明确路径
)
// ✅ 备选方案:映射表
const components = {
chart: () => import('./Chart.vue'),
table: () => import('./Table.vue')
}
📈 优化效果对比
| 阶段 | 首屏资源 | HTTP请求 | 加载时机 | 用户体验 |
|---|---|---|---|---|
| 初始 | 全部模块 | 大量 | 同时加载 | 首屏慢 |
| 分包 | 分割文件 | 多个 | 同时加载 | 略有改善 |
| 异步组件 | 首屏必要 | 多个 | 预加载 | 首屏快,但流量浪费 |
| 懒加载 | 首屏必要 | 按需 | 滚动触发 | 最优 |
🎯 关键技术点
1. IntersectionObserver API
// 监听元素进入视口
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadContent() // 触发加载
}
}, {
rootMargin: '100px', // 提前100px加载
threshold: 0.1 // 10%可见时触发
})
2. Vue 3 异步组件
// 正确的异步组件定义
const AsyncComponent = defineAsyncComponent({
loader: () => import('./Component.vue'),
loadingComponent: LoadingSpinner, // 加载中组件
errorComponent: ErrorComponent, // 错误组件
delay: 200, // 延迟显示loading
timeout: 10000 // 超时时间
})
3. Suspense 组件
<!-- 配合Suspense处理加载状态 -->
<LazyContainer>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<SkeletonLoader /> <!-- 专属骨架屏 -->
</template>
</Suspense>
</LazyContainer>
🔧 最终配置
vite.config.js
export default {
build: {
rollupOptions: {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
utils: ['lodash-es', 'axios']
}
}
}
}
}
最佳实践总结
- 首屏关键模块:直接import,优先加载
- 非首屏模块:LazyContainer + 异步组件
- 分包策略:基础库单独分包
- 加载状态:Suspense + 骨架屏
- 错误处理:异步组件的errorComponent
📊 性能指标提升
- ✅ 首屏加载时间减少 40-60%
- ✅ 初始包体积减小 50%+
- ✅ 按需加载,节省用户流量
- ✅ 滚动体验更流畅
- ✅ 代码可维护性更好
🎯 后续优化方向
- 预加载:对即将进入视口的模块预加载
- 请求合并:对相邻模块合并加载
- 缓存策略:合理配置HTTP缓存
- 图片懒加载:配合vue-lazyload
- 虚拟滚动:超长列表优化
总结
今天完成了从整体加载到按需加载的架构优化,通过结合Vite分包、Vue异步组件和IntersectionObserver,实现了真正的懒加载。核心收获是理解了组件渲染时机和代码分割的实际应用,解决了动态导入的构建问题,为后续性能优化打下了坚实基础。