Vue应用级性能分析与优化之Vite打包优化
按需加载
按需加载是优化Vue应用性能的核心策略之一,通过将代码分割成更小的chunks,实现真正的按需引入,减少初始包体积,提升首屏加载速度。
路由懒加载
Vue Router支持异步组件,结合Vite的动态导入能力,可以实现路由级别的代码分割。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
// 路由级别的代码分割
// 这会为该路由生成单独的 chunk (About.[hash].js)
component: () => import('@/views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 为相关的路由分组,生成命名的chunk
webpackChunkName: 'dashboard-group'
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
组件懒加载
除了路由级别,我们还可以在组件级别实现按需加载:
// 组件内的异步组件
<template>
<div>
<h1>主页面</h1>
<!-- 重量级组件按需加载 -->
<Suspense>
<template #default>
<AsyncHeavyComponent v-if="showHeavyComponent" />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// 异步组件定义
const AsyncHeavyComponent = defineAsyncComponent({
loader: () => import('@/components/HeavyComponent.vue'),
loadingComponent: () => import('@/components/Loading.vue'),
errorComponent: () => import('@/components/Error.vue'),
delay: 200,
timeout: 3000
})
const showHeavyComponent = ref(false)
</script>
第三方库按需引入
对于大型第三方库,建议使用按需引入的方式:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
// 将第三方库单独打包
vendor: ['vue', 'vue-router'],
ui: ['element-plus'],
utils: ['lodash-es', 'dayjs']
}
}
}
}
})
// 使用插件实现自动按需引入
// 安装: npm install unplugin-auto-import unplugin-vue-components -D
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
]
})
动态导入最佳实践
// 动态导入工具函数
const loadComponent = (componentPath) => {
return defineAsyncComponent({
loader: () => import(componentPath),
loadingComponent: {
template: '<div class="loading">组件加载中...</div>'
},
errorComponent: {
template: '<div class="error">组件加载失败</div>'
},
delay: 100,
timeout: 10000
})
}
// 条件性动态导入
const loadChartComponent = async (chartType) => {
switch (chartType) {
case 'line':
return await import('@/components/charts/LineChart.vue')
case 'bar':
return await import('@/components/charts/BarChart.vue')
case 'pie':
return await import('@/components/charts/PieChart.vue')
default:
throw new Error('Unknown chart type')
}
}
自动依赖预构建
Vite的自动依赖预构建是其快速冷启动的核心特性。通过ESBuild将CommonJS/UMD格式的依赖转换为ESM格式,并进行打包优化,显著提升开发体验。
预构建工作原理
graph TD
A[启动开发服务器] --> B[扫描入口文件]
B --> C[分析依赖关系]
C --> D[识别需要预构建的依赖]
D --> E[ESBuild预构建]
E --> F[生成.vite/deps目录]
F --> G[缓存预构建结果]
G --> H[服务器就绪]
I[代码变更] --> J{依赖变化?}
J -->|是| K[重新预构建]
J -->|否| L[直接使用缓存]
K --> F
L --> H
预构建配置优化
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
// 明确指定需要预构建的依赖
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
// 深层导入的模块
'element-plus/es/components/button/style/css',
'element-plus/es/components/input/style/css'
],
// 排除某些依赖的预构建
exclude: [
// 已经是ESM格式的包
'some-esm-package',
// 动态导入的包
'@/utils/dynamic-module'
],
// ESBuild 选项
esbuildOptions: {
// 设置目标环境
target: 'es2020',
// 定义全局变量
define: {
global: 'globalThis'
},
// 支持的文件扩展名
resolveExtensions: ['.mjs', '.js', '.ts', '.json']
},
// 强制重新预构建
force: false
}
})
条件预构建配置
// 根据环境动态配置预构建
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production'
const isDevelopment = command === 'serve'
return {
optimizeDeps: {
include: [
'vue',
'vue-router',
// 开发环境额外预构建调试工具
...(isDevelopment ? ['vue-devtools'] : []),
// 生产环境预构建所有依赖以提高性能
...(isProduction ? ['all-dependencies'] : [])
]
}
}
})
大型依赖库优化
// 针对大型UI库的预构建优化
export default defineConfig({
optimizeDeps: {
include: [
// Element Plus 按需预构建
'element-plus/es/locale/lang/zh-cn',
'element-plus/es/locale/lang/en',
// Ant Design Vue 核心组件
'ant-design-vue/es/button',
'ant-design-vue/es/input',
'ant-design-vue/es/table',
// 图表库预构建
'echarts/core',
'echarts/charts/LineChart',
'echarts/charts/BarChart',
'echarts/components/TooltipComponent',
'echarts/components/GridComponent',
'echarts/renderers/CanvasRenderer'
]
}
})
预构建缓存管理
// 缓存策略配置
export default defineConfig({
cacheDir: '.vite', // 缓存目录
optimizeDeps: {
// 缓存策略
force: process.env.FORCE_PREBUILD === 'true',
// 入口点配置
entries: [
'src/main.ts',
'src/pages/*/main.ts', // 多页面应用
'src/workers/*.ts' // Web Workers
]
}
})
预构建性能监控
// 预构建性能监控插件
function prebuildMonitorPlugin() {
return {
name: 'prebuild-monitor',
buildStart() {
console.time('预构建时间')
},
buildEnd() {
console.timeEnd('预构建时间')
},
configResolved(config) {
if (config.command === 'serve') {
const originalOptimizeDeps = config.optimizeDeps || {}
console.log('预构建配置:', {
include: originalOptimizeDeps.include?.length || 0,
exclude: originalOptimizeDeps.exclude?.length || 0,
entries: originalOptimizeDeps.entries?.length || 0
})
}
}
}
}
预构建故障排除
// 预构建问题诊断工具
export default defineConfig({
optimizeDeps: {
// 启用详细日志
include: ['problematic-package'],
esbuildOptions: {
// 保留函数名用于调试
keepNames: true,
// 生成 source map
sourcemap: true
}
},
// 开发服务器配置
server: {
// 依赖预构建完成前阻止页面加载
preTransformRequests: false
},
// 日志级别
logLevel: 'info'
})
模块热更新
模块热更新(HMR)是Vite开发体验的核心特性,通过保持应用状态的同时更新模块,实现真正的"热重载",极大提升开发效率。
HMR工作原理
graph TD
A[文件变更] --> B[文件系统监听]
B --> C[识别变更类型]
C --> D{模块类型判断}
D -->|Vue SFC| E[Vue组件热更新]
D -->|CSS| F[样式热更新]
D -->|JS/TS| G[模块热更新]
E --> H[保持组件状态]
F --> I[即时样式替换]
G --> J[状态检查]
H --> K[WebSocket推送]
I --> K
J --> L{是否可热更新}
L -->|是| K
L -->|否| M[完整页面刷新]
K --> N[客户端接收]
N --> O[应用热更新]
O --> P[页面局部更新]
HMR配置优化
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
// Vue热更新选项
reactivityTransform: true,
// 自定义块热更新
customElement: true
})
],
server: {
// HMR配置
hmr: {
// 自定义HMR端口
port: 24678,
// 自定义主机
host: 'localhost',
// 覆盖WebSocket服务器选项
server: {
// 使用自定义服务器
}
},
// 监听文件变化配置
watch: {
// 忽略某些文件的监听
ignored: ['**/node_modules/**', '**/.git/**'],
// 使用轮询(在某些系统上更稳定)
usePolling: false,
// 轮询间隔
interval: 100
}
}
})
Vue组件HMR优化
// Vue组件热更新边界设置
// ComponentA.vue
<template>
<div class="component-a">
<h2>{{ title }}</h2>
<ComponentB />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ComponentB from './ComponentB.vue'
// 热更新保持的状态
const title = ref('组件A')
const formData = ref({
name: '',
email: ''
})
// 热更新时需要重新执行的逻辑
onMounted(() => {
console.log('组件A挂载')
})
// 手动定义热更新边界
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log('ComponentA 热更新')
})
// 保存状态
import.meta.hot.data.formData = formData.value
// 恢复状态
if (import.meta.hot.data.formData) {
formData.value = import.meta.hot.data.formData
}
}
</script>
自定义HMR边界
// 状态管理的HMR处理
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: []
}),
actions: {
increment() {
this.count++
this.history.push(`增加到 ${this.count}`)
}
}
})
// HMR支持
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 热更新时保持状态
const currentState = useCounterStore().$state
newModule.useCounterStore().$patch(currentState)
})
}
样式热更新优化
// CSS模块热更新
// styles/theme.module.css
.primary {
color: var(--primary-color, #007bff);
transition: color 0.3s ease;
}
.secondary {
color: var(--secondary-color, #6c757d);
}
// Vue组件中的样式热更新
<template>
<div :class="$style.container">
<button :class="$style.primary">主要按钮</button>
</div>
</template>
<style module>
.container {
padding: 20px;
/* 样式变更会立即生效,无需刷新页面 */
}
.primary {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
/* 修改这里的样式会触发热更新 */
}
</style>
<script setup>
// 样式热更新监听
if (import.meta.hot) {
import.meta.hot.accept()
}
</script>
HMR性能优化
// HMR性能监控插件
function hmrPerformancePlugin() {
let updateCount = 0
let totalTime = 0
return {
name: 'hmr-performance',
handleHotUpdate(ctx) {
const start = Date.now()
return new Promise((resolve) => {
setTimeout(() => {
const duration = Date.now() - start
updateCount++
totalTime += duration
console.log(`HMR更新 #${updateCount}: ${duration}ms`)
console.log(`平均更新时间: ${(totalTime / updateCount).toFixed(2)}ms`)
resolve()
}, 0)
})
}
}
}
// 大文件HMR优化
export default defineConfig({
plugins: [
vue(),
hmrPerformancePlugin(),
{
name: 'large-file-hmr',
handleHotUpdate(ctx) {
const { file, modules } = ctx
// 对大文件采用更精细的更新策略
if (ctx.file.includes('large-component')) {
return modules.filter(mod =>
mod.id?.includes('specific-part')
)
}
}
}
]
})
HMR故障排除
// HMR调试工具
function hmrDebugPlugin() {
return {
name: 'hmr-debug',
configureServer(server) {
server.ws.on('connection', () => {
console.log('HMR WebSocket连接建立')
})
server.ws.on('error', (err) => {
console.error('HMR WebSocket错误:', err)
})
},
handleHotUpdate(ctx) {
console.log('文件变更:', {
file: ctx.file,
timestamp: new Date().toISOString(),
modules: ctx.modules.length
})
// 检测循环依赖
ctx.modules.forEach(mod => {
if (mod.id && hasCircularDependency(mod)) {
console.warn('检测到循环依赖:', mod.id)
}
})
}
}
}
function hasCircularDependency(module, visited = new Set()) {
if (visited.has(module.id)) return true
visited.add(module.id)
return module.importers.some(importer =>
hasCircularDependency(importer, visited)
)
}
HMR最佳实践
// 组件设计最佳实践
// 好的HMR支持
<script setup>
import { ref, computed } from 'vue'
// 使用组合式API,状态更容易保持
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
// 副作用应该在适当的生命周期中处理
onMounted(() => {
// 初始化逻辑
})
// 避免在模块顶层执行副作用
// ❌ 不好的做法
// const socket = new WebSocket('ws://localhost:8080')
// ✅ 好的做法
const initializeSocket = () => {
return new WebSocket('ws://localhost:8080')
}
</script>
持久化缓存
持久化缓存是现代Web应用性能优化的关键策略,通过合理的缓存机制,可以显著减少重复资源的加载时间,提升用户体验。
缓存策略概览
graph TD
A[浏览器请求] --> B{缓存检查}
B -->|命中| C[直接返回缓存]
B -->|未命中| D[发起网络请求]
D --> E[服务器响应]
E --> F[设置缓存头]
F --> G[存储到缓存]
G --> H[返回资源]
I[文件hash变化] --> J[缓存失效]
J --> K[重新请求]
K --> L[更新缓存]
M[缓存清理策略] --> N{存储空间}
N -->|充足| O[保持缓存]
N -->|不足| P[LRU清理]
Vite文件Hash策略
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// 文件名hash策略
rollupOptions: {
output: {
// 入口文件hash
entryFileNames: 'assets/[name].[hash].js',
// chunk文件hash
chunkFileNames: 'assets/[name].[hash].js',
// 资源文件hash
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
let extType = info[info.length - 1]
// 根据文件类型设置不同的hash策略
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'media'
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'img'
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'fonts'
}
return `assets/${extType}/[name].[hash][extname]`
},
// 手动chunk分割
manualChunks: {
// 第三方库单独打包
vendor: ['vue', 'vue-router'],
// UI库单独打包
ui: ['element-plus', 'ant-design-vue'],
// 工具库单独打包
utils: ['lodash-es', 'dayjs', 'axios']
}
}
},
// 启用CSS代码分割
cssCodeSplit: true,
// 压缩选项
minify: 'terser',
terserOptions: {
compress: {
// 移除console和debugger
drop_console: true,
drop_debugger: true
}
}
}
})
智能缓存配置
// 高级缓存配置
export default defineConfig({
build: {
rollupOptions: {
output: {
// 智能chunk命名
chunkFileNames: (chunkInfo) => {
// 根据chunk大小决定缓存策略
if (chunkInfo.facadeModuleId?.includes('node_modules')) {
// 第三方库使用内容hash
return 'vendor/[name].[contenthash:8].js'
} else if (chunkInfo.name?.includes('page-')) {
// 页面级组件使用短hash
return 'pages/[name].[hash:6].js'
} else {
// 其他使用标准hash
return 'chunks/[name].[hash].js'
}
},
// 动态chunk分割策略
manualChunks(id) {
// 第三方库分组
if (id.includes('node_modules')) {
// Vue生态系统
if (id.includes('vue') || id.includes('pinia')) {
return 'vue-ecosystem'
}
// UI组件库
if (id.includes('element-plus') || id.includes('ant-design')) {
return 'ui-library'
}
// 工具库
if (id.includes('lodash') || id.includes('moment') || id.includes('dayjs')) {
return 'utils'
}
// 图表库
if (id.includes('echarts') || id.includes('d3')) {
return 'charts'
}
// 其他第三方库
return 'vendor'
}
// 业务模块分组
if (id.includes('/src/views/')) {
const pathSegments = id.split('/')
const viewDir = pathSegments[pathSegments.indexOf('views') + 1]
return `page-${viewDir}`
}
}
}
}
}
})
HTTP缓存头设置
// 开发服务器缓存配置
export default defineConfig({
server: {
// 开发环境缓存设置
headers: {
// 静态资源长期缓存
'Cache-Control': 'public, max-age=31536000'
}
},
preview: {
// 预览环境缓存设置
headers: {
'Cache-Control': 'public, max-age=604800'
}
}
})
# nginx生产环境缓存配置
server {
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# 静态资源长期缓存
expires 1y;
add_header Cache-Control "public, immutable";
# 启用gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
location /assets/ {
# hash文件永久缓存
expires max;
add_header Cache-Control "public, immutable";
}
location / {
# HTML文件不缓存,确保更新及时
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
}
}
Service Worker缓存策略
// sw.js - Service Worker缓存实现
const CACHE_NAME = 'vite-app-v1'
const STATIC_CACHE = 'static-v1'
const DYNAMIC_CACHE = 'dynamic-v1'
// 缓存策略配置
const cacheStrategies = {
// 静态资源:缓存优先
static: [
/\/assets\//,
/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/
],
// API请求:网络优先
api: [
/\/api\//
],
// 页面:网络优先,缓存备用
pages: [
/\.html$/,
/\/$/
]
}
// 安装事件
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
return cache.addAll([
'/',
'/manifest.json',
// 关键CSS和JS文件
'/assets/index.css',
'/assets/index.js'
])
})
)
})
// 获取事件
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// 静态资源缓存策略
if (cacheStrategies.static.some(pattern => pattern.test(url.pathname))) {
event.respondWith(cacheFirst(request))
}
// API请求策略
else if (cacheStrategies.api.some(pattern => pattern.test(url.pathname))) {
event.respondWith(networkFirst(request))
}
// 页面请求策略
else if (cacheStrategies.pages.some(pattern => pattern.test(url.pathname))) {
event.respondWith(staleWhileRevalidate(request))
}
})
// 缓存优先策略
async function cacheFirst(request) {
const cachedResponse = await caches.match(request)
if (cachedResponse) {
return cachedResponse
}
try {
const networkResponse = await fetch(request)
const cache = await caches.open(STATIC_CACHE)
cache.put(request, networkResponse.clone())
return networkResponse
} catch (error) {
console.error('缓存优先策略失败:', error)
throw error
}
}
// 网络优先策略
async function networkFirst(request) {
try {
const networkResponse = await fetch(request)
const cache = await caches.open(DYNAMIC_CACHE)
cache.put(request, networkResponse.clone())
return networkResponse
} catch (error) {
const cachedResponse = await caches.match(request)
if (cachedResponse) {
return cachedResponse
}
throw error
}
}
// 过期重新验证策略
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE)
const cachedResponse = await cache.match(request)
const fetchPromise = fetch(request).then((networkResponse) => {
cache.put(request, networkResponse.clone())
return networkResponse
})
return cachedResponse || fetchPromise
}
客户端缓存管理
// 客户端缓存工具类
class CacheManager {
constructor() {
this.localStorage = window.localStorage
this.sessionStorage = window.sessionStorage
this.indexedDB = null
this.initIndexedDB()
}
// 初始化IndexedDB
async initIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ViteAppCache', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.indexedDB = request.result
resolve(this.indexedDB)
}
request.onupgradeneeded = (event) => {
const db = event.target.result
if (!db.objectStoreNames.contains('cache')) {
const store = db.createObjectStore('cache', { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false })
}
}
})
}
// 设置带过期时间的缓存
setCache(key, data, ttl = 3600000) { // 默认1小时
const item = {
key,
data,
timestamp: Date.now(),
ttl
}
try {
// 小数据存localStorage
if (JSON.stringify(data).length < 5000) {
this.localStorage.setItem(key, JSON.stringify(item))
} else {
// 大数据存IndexedDB
this.setIndexedDBCache(key, item)
}
} catch (error) {
console.warn('缓存设置失败:', error)
}
}
// 获取缓存
async getCache(key) {
try {
// 先检查localStorage
const localItem = this.localStorage.getItem(key)
if (localItem) {
const parsed = JSON.parse(localItem)
if (this.isValidCache(parsed)) {
return parsed.data
} else {
this.localStorage.removeItem(key)
}
}
// 检查IndexedDB
const indexedItem = await this.getIndexedDBCache(key)
if (indexedItem && this.isValidCache(indexedItem)) {
return indexedItem.data
}
return null
} catch (error) {
console.warn('缓存获取失败:', error)
return null
}
}
// 检查缓存是否有效
isValidCache(item) {
const now = Date.now()
return (now - item.timestamp) < item.ttl
}
// IndexedDB操作
async setIndexedDBCache(key, item) {
if (!this.indexedDB) await this.initIndexedDB()
const transaction = this.indexedDB.transaction(['cache'], 'readwrite')
const store = transaction.objectStore('cache')
store.put(item)
}
async getIndexedDBCache(key) {
if (!this.indexedDB) await this.initIndexedDB()
return new Promise((resolve, reject) => {
const transaction = this.indexedDB.transaction(['cache'], 'readonly')
const store = transaction.objectStore('cache')
const request = store.get(key)
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
// 清理过期缓存
async cleanExpiredCache() {
// 清理localStorage
Object.keys(this.localStorage).forEach(key => {
try {
const item = JSON.parse(this.localStorage.getItem(key))
if (item.timestamp && !this.isValidCache(item)) {
this.localStorage.removeItem(key)
}
} catch (error) {
// 非缓存数据,跳过
}
})
// 清理IndexedDB
if (this.indexedDB) {
const transaction = this.indexedDB.transaction(['cache'], 'readwrite')
const store = transaction.objectStore('cache')
const index = store.index('timestamp')
index.openCursor().onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
if (!this.isValidCache(cursor.value)) {
store.delete(cursor.value.key)
}
cursor.continue()
}
}
}
}
}
// 全局缓存管理器
const cacheManager = new CacheManager()
// 定期清理过期缓存
setInterval(() => {
cacheManager.cleanExpiredCache()
}, 300000) // 每5分钟清理一次
export default cacheManager
Tree-shaking 和 Dead Code Elimination
Tree-shaking是现代JavaScript打包工具的核心特性,通过静态分析消除未使用的代码,显著减少最终包体积。Vite基于Rollup的Tree-shaking能力,提供了出色的代码优化效果。
Tree-shaking工作原理
graph TD
A[源代码分析] --> B[构建依赖图]
B --> C[标记使用的导出]
C --> D[识别副作用]
D --> E[删除未使用代码]
E --> F[生成优化后代码]
G[ES模块静态分析] --> H{导入类型}
H -->|命名导入| I[精确标记]
H -->|默认导入| J[整体标记]
H -->|动态导入| K[运行时保留]
L[副作用检测] --> M{代码类型}
M -->|纯函数| N[可安全删除]
M -->|有副作用| O[必须保留]
M -->|不确定| P[保守保留]
Vite Tree-shaking配置
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// 启用Tree-shaking
minify: 'terser',
terserOptions: {
compress: {
// 删除未使用的函数参数
unused: true,
// 删除未引用的函数和变量
dead_code: true,
// 删除debugger语句
drop_debugger: true,
// 删除console语句
drop_console: true,
// 简化布尔值
booleans: true,
// 删除未使用的导入
side_effects: false
},
mangle: {
// 混淆变量名
properties: {
regex: /^_/
}
}
},
rollupOptions: {
// 外部依赖不参与Tree-shaking
external: ['vue', 'vue-router'],
output: {
// 全局变量映射
globals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
},
// Tree-shaking插件配置
plugins: [
{
name: 'tree-shaking-analyzer',
generateBundle(options, bundle) {
// 分析Tree-shaking效果
Object.keys(bundle).forEach(fileName => {
const chunk = bundle[fileName]
if (chunk.type === 'chunk') {
console.log(`${fileName}: ${chunk.code.length} bytes`)
}
})
}
}
]
}
}
})
ES模块最佳实践
// ✅ 支持Tree-shaking的模块写法
// utils/math.js
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export const multiply = (a, b) => a * b
export const divide = (a, b) => a / b
// 纯函数,无副作用
export const calculateArea = (radius) => Math.PI * radius * radius
// 标记纯函数
export /*#__PURE__*/ const createCalculator = () => {
return {
add,
subtract,
multiply,
divide
}
}
// ❌ 不支持Tree-shaking的写法
// utils/math-bad.js
const MathUtils = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b
}
// 副作用代码
console.log('Math utils loaded') // 有副作用
export default MathUtils
第三方库Tree-shaking优化
// package.json 配置
{
"name": "my-library",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"sideEffects": false, // 标记为无副作用
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./utils": {
"import": "./dist/utils.esm.js",
"require": "./dist/utils.js"
}
}
}
// 按需导入第三方库
// ✅ 支持Tree-shaking
import { debounce, throttle } from 'lodash-es'
import { format } from 'date-fns'
import { Button, Input } from 'element-plus'
// ❌ 导入整个库
import _ from 'lodash'
import * as dateFns from 'date-fns'
import ElementPlus from 'element-plus'
自定义Tree-shaking插件
// plugins/tree-shaking-optimizer.js
export function treeShakingOptimizer() {
return {
name: 'tree-shaking-optimizer',
// 分析阶段
buildStart() {
this.usedExports = new Map()
this.moduleGraph = new Map()
},
// 模块解析阶段
resolveId(id, importer) {
if (importer) {
if (!this.moduleGraph.has(importer)) {
this.moduleGraph.set(importer, new Set())
}
this.moduleGraph.get(importer).add(id)
}
},
// 转换阶段
transform(code, id) {
// 分析导入导出
const imports = this.extractImports(code)
const exports = this.extractExports(code)
imports.forEach(imp => {
if (!this.usedExports.has(imp.source)) {
this.usedExports.set(imp.source, new Set())
}
imp.specifiers.forEach(spec => {
this.usedExports.get(imp.source).add(spec)
})
})
return null
},
// 生成阶段
generateBundle() {
// 输出Tree-shaking报告
console.log('Tree-shaking分析报告:')
this.usedExports.forEach((exports, module) => {
console.log(`${module}: ${Array.from(exports).join(', ')}`)
})
},
extractImports(code) {
const importRegex = /import\s+(?:{\s*([^}]+)\s*}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g
const imports = []
let match
while ((match = importRegex.exec(code)) !== null) {
if (match[1]) {
// 命名导入
const specifiers = match[1].split(',').map(s => s.trim())
imports.push({
source: match[2],
specifiers
})
}
}
return imports
},
extractExports(code) {
const exportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g
const exports = []
let match
while ((match = exportRegex.exec(code)) !== null) {
exports.push(match[1])
}
return exports
}
}
}
Vue组件Tree-shaking优化
// 组件按需导入
// components/index.js
export { default as BaseButton } from './BaseButton.vue'
export { default as BaseInput } from './BaseInput.vue'
export { default as BaseModal } from './BaseModal.vue'
export { default as BaseTable } from './BaseTable.vue'
// 使用时只导入需要的组件
import { BaseButton, BaseInput } from '@/components'
// Vue组件Tree-shaking友好写法
// BaseButton.vue
<template>
<button
:class="buttonClasses"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup>
import { computed } from 'vue'
// 明确定义props
const props = defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
},
size: {
type: String,
default: 'medium'
},
disabled: {
type: Boolean,
default: false
}
})
// 明确定义emits
const emit = defineEmits(['click'])
// 计算属性,支持Tree-shaking
const buttonClasses = computed(() => {
return [
'btn',
`btn--${props.type}`,
`btn--${props.size}`,
{
'btn--disabled': props.disabled
}
]
})
// 事件处理,支持Tree-shaking
const handleClick = (event) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
工具类Tree-shaking优化
// utils/index.js - Tree-shaking友好的工具类
// 每个函数独立导出
export const isObject = (value) => {
return value !== null && typeof value === 'object'
}
export const isArray = (value) => {
return Array.isArray(value)
}
export const isEmpty = (value) => {
if (value == null) return true
if (isArray(value) || typeof value === 'string') {
return value.length === 0
}
if (isObject(value)) {
return Object.keys(value).length === 0
}
return false
}
// 复合函数标记为纯函数
export /*#__PURE__*/ const createValidator = (rules) => {
return (value) => {
return rules.every(rule => rule(value))
}
}
// 副作用代码单独处理
let globalConfig = null
export const setGlobalConfig = (config) => {
globalConfig = config
}
export const getGlobalConfig = () => {
return globalConfig
}
Tree-shaking效果分析
// 分析工具
function analyzeTreeShaking() {
return {
name: 'analyze-tree-shaking',
generateBundle(options, bundle) {
const analysis = {
totalModules: 0,
usedModules: 0,
eliminatedBytes: 0,
modules: []
}
Object.keys(bundle).forEach(fileName => {
const chunk = bundle[fileName]
if (chunk.type === 'chunk') {
analysis.totalModules += chunk.modules ? Object.keys(chunk.modules).length : 0
analysis.usedModules += chunk.modules ? Object.keys(chunk.modules).filter(
moduleId => !chunk.modules[moduleId].removedExports?.length
).length : 0
analysis.modules.push({
name: fileName,
size: chunk.code.length,
modules: chunk.modules ? Object.keys(chunk.modules).length : 0
})
}
})
console.log('Tree-shaking效果分析:')
console.log(`总模块数: ${analysis.totalModules}`)
console.log(`保留模块数: ${analysis.usedModules}`)
console.log(`消除率: ${((analysis.totalModules - analysis.usedModules) / analysis.totalModules * 100).toFixed(2)}%`)
// 生成分析报告
this.emitFile({
type: 'asset',
fileName: 'tree-shaking-report.json',
source: JSON.stringify(analysis, null, 2)
})
}
}
}
副作用处理最佳实践
// 正确处理副作用
// polyfills.js
/*#__PURE__*/
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) !== -1
}
}
// 条件性副作用
export const initPolyfills = /*#__PURE__*/ () => {
// 只有调用时才执行副作用
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) !== -1
}
}
}
// CSS副作用处理
// styles/index.js
import './reset.css' // 副作用导入
import './variables.css' // 副作用导入
// 在package.json中标记
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/styles/**/*"
]
}
优化验证和测试
// 测试Tree-shaking效果
// tests/tree-shaking.test.js
import { build } from 'vite'
import { fileURLToPath } from 'url'
import { dirname, resolve } from 'path'
import { readFileSync } from 'fs'
const __dirname = dirname(fileURLToPath(import.meta.url))
describe('Tree-shaking测试', () => {
test('应该消除未使用的工具函数', async () => {
const result = await build({
root: resolve(__dirname, '../fixtures'),
build: {
write: false,
minify: false,
rollupOptions: {
input: resolve(__dirname, '../fixtures/tree-shaking-test.js')
}
}
})
const output = result.output[0]
const code = output.code
// 验证未使用的函数被消除
expect(code).not.toContain('unusedFunction')
expect(code).toContain('usedFunction')
})
test('应该保留有副作用的代码', async () => {
// 测试副作用代码保留
const result = await build({
root: resolve(__dirname, '../fixtures'),
build: {
write: false,
rollupOptions: {
input: resolve(__dirname, '../fixtures/side-effects-test.js')
}
}
})
const output = result.output[0]
const code = output.code
// 验证副作用代码被保留
expect(code).toContain('console.log')
expect(code).toContain('window.globalVariable')
})
})