Vue应用级性能分析与优化之Vite打包优化

204 阅读8分钟

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')
  })
})