Skip to content

Vite 分包策略 - 代码分割与按需加载

一、核心要点速览

💡 核心考点

  • 分包目的: 减少初始加载体积,提升首屏速度
  • 核心原理: Rollup 的代码分割机制 + 动态导入
  • 常用策略: 路由分包、组件懒加载、第三方库拆分
  • 配置关键: manualChunks + dynamicImport

二、为什么需要分包

问题分析

单 Bundle 的问题:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 初始加载慢
  所有代码打包成一个文件
  用户需要下载全部代码才能使用
  
  例如:app.js (2MB)
  └─ 首页只需要 20% 的代码
  └─ 其他 80% 被浪费

❌ 缓存效率低
  任何代码修改都会改变整个 bundle 的 hash
  用户需要重新下载所有代码

❌ 内存占用高
  大量未使用的代码占用内存
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

分包的优势

分包后的效果:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ 减少初始加载
  只加载当前页面需要的代码
  首屏加载时间 ↓ 50-70%

✓ 按需加载
  其他路由代码延迟加载
  节省带宽和流量

✓ 缓存优化
  每个 chunk 独立 hash
  更新时只重新下载变化的部分

✓ 并行加载
  多个小文件可以并行下载
  利用浏览器并发能力
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

三、Vite 分包原理

Rollup 代码分割机制

┌──────────────────────────────────────────────────────────┐
│              Vite/Rollup 分包流程                         │
└──────────────────────────────────────────────────────────┘

构建流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 解析入口文件

2. 分析静态 import
   ├─ 基础依赖 → vendor chunk
   ├─ 页面组件 → pages chunk
   └─ 共享代码 → common chunk

3. 分析动态 import()
   └─ 自动创建独立 chunk

4. 应用 manualChunks 规则
   └─ 按配置强制拆分

5. 生成最终 chunks
   ├─ index.html
   ├─ assets/
   │   ├── index.a1b2c3.js (入口)
   │   ├── vendor.d4e5f6.js (第三方库)
   │   ├── Home.g7h8i9.js (路由页面)
   │   └── common.j0k1l2.js (共享代码)
   └─ ...

关键点:
  ✓ 静态 import 自动分析
  ✓ 动态 import() 自动分割
  ✓ manualChunks 手动控制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Chunk 命名规则

javascript
// 默认命名格式
[文件名].[hash].[ext]

示例:
  index.a1b2c3d4.js      // 入口文件
  vendor.e5f6g7h8.js     // node_modules 依赖
  Home.i9j0k1l2.js       // 动态导入的页面
  common.m3n4o5p6.js     // 共享代码

带 hash 的好处:
  ✓ 长期缓存(hash 不变则缓存有效)
  ✓ 更新检测(内容变化 hash 变化)
  ✓ 避免缓存冲突

四、分包策略详解

策略一:路由分包(最常用)

javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue') // 动态导入,自动分包
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue') // 独立 chunk
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue') // 独立 chunk
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

// 构建结果:
// - dist/assets/Home.xxx.js
// - dist/assets/About.yyy.js
// - dist/assets/User.zzz.js

效果对比:

方案初始加载About 页面User 页面
不分包2MB0ms0ms
路由分包500KB300KB400KB
提升↓ 75%按需加载按需加载

策略二:组件懒加载

vue
<!-- 重组件按需加载 -->
<template>
  <div>
    <!-- 方式 1: 动态导入组件 -->
    <component :is="HeavyComponent" />
    
    <!-- 方式 2: 异步组件(推荐) -->
    <AsyncChart />
    
    <!-- 方式 3: 带 loading 状态的异步组件 -->
    <AsyncEditor />
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
  components: {
    // 基础异步组件
    HeavyComponent: () => import('@/components/HeavyComponent.vue'),
    
    // 带配置的异步组件
    AsyncChart: defineAsyncComponent({
      loader: () => import('@/components/Chart.vue'),
      loadingComponent: LoadingSpinner,
      delay: 200, // 显示 loading 前的延迟
      timeout: 3000 // 超时时间
    }),
    
    // 复杂场景
    AsyncEditor: defineAsyncComponent(() => ({
      component: import('@/components/Editor.vue'),
      loading: LoadingComponent,
      error: ErrorComponent,
      delay: 200,
      timeout: 5000
    }))
  }
}
</script>

策略三:第三方库拆分

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 1. Vue 相关单独拆分
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          
          // 2. UI 库拆分
          'element-plus': ['element-plus'],
          'antd': ['ant-design-vue'],
          
          // 3. 工具库拆分
          'lodash': ['lodash-es'],
          'dayjs': ['dayjs'],
          
          // 4. 图表库(通常较大)
          'echarts': ['echarts'],
          
          // 5. 富文本编辑器
          'editor': ['@wangeditor/editor', '@wangeditor/editor-for-vue']
        }
      }
    }
  }
})

// 构建结果:
// - dist/assets/vue-vendor.abc123.js
// - dist/assets/element-plus.def456.js
// - dist/assets/lodash.ghi789.js
// - dist/assets/echarts.jkl012.js

进阶:函数形式更灵活

javascript
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 1. node_modules 中的依赖
          if (id.includes('node_modules')) {
            // Vue 生态
            if (id.includes('vue') || id.includes('pinia')) {
              return 'vue-vendor'
            }
            
            // UI 组件库
            if (id.includes('element-plus')) {
              return 'element-plus'
            }
            
            // 大型图表库
            if (id.includes('echarts')) {
              return 'charts'
            }
            
            // 其他统一放入 vendor
            return 'vendor'
          }
          
          // 2. 项目中的共享组件
          if (id.includes('src/components/common')) {
            return 'common-components'
          }
          
          // 3. 工具函数
          if (id.includes('src/utils')) {
            return 'utils'
          }
        }
      }
    }
  }
})

策略四:大文件单独拆分

javascript
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id, { getModuleInfo }) {
          // 获取模块大小
          const moduleSize = getModuleInfo(id)?.size || 0
          
          // 超过 50KB 的文件单独拆分
          if (moduleSize > 50 * 1024) {
            const fileName = id.split('/').pop()?.split('.')[0]
            return `large-${fileName}`
          }
          
          // 特定页面单独拆分
          if (id.includes('pages/heavy-page')) {
            return 'heavy-page'
          }
        }
      }
    }
  }
})

五、完整配置示例

Vue 项目最佳实践

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  
  build: {
    // 目标环境
    target: 'es2015',
    
    // 输出目录
    outDir: 'dist',
    
    // 代码分割配置
    rollupOptions: {
      output: {
        // 自定义 chunk 命名
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
        
        // 手动分包
        manualChunks: {
          // 1. 框架核心
          'framework': [
            'vue',
            'vue-router',
            'pinia'
          ],
          
          // 2. UI 库
          'ui': [
            'element-plus',
            '@element-plus/icons-vue'
          ],
          
          // 3. 请求库
          'request': [
            'axios'
          ],
          
          // 4. 工具库
          'utils': [
            'lodash-es',
            'dayjs',
            'crypto-js'
          ],
          
          // 5. 可视化库
          'charts': [
            'echarts',
            'zrender'
          ]
        }
      }
    },
    
    // 分包大小限制
    chunkSizeWarningLimit: 500, // KB
    
    // Gzip 压缩
    gzipSize: true,
    
    // 代码分割优化
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // 生产环境移除 console
        drop_debugger: true
      }
    }
  }
})

React 项目配置

javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React 核心
          'react-core': ['react', 'react-dom', 'react-router-dom'],
          
          // 状态管理
          'state': ['redux', 'react-redux', '@reduxjs/toolkit'],
          
          // UI 库
          'antd': ['antd', '@ant-design/icons'],
          
          // 工具
          'utils': ['lodash-es', 'dayjs']
        }
      }
    }
  }
})

六、分包效果分析

性能对比

┌──────────────────────────────────────────────────────────┐
│                  分包效果对比                             │
└──────────────────────────────────────────────────────────┘

测试项目:中型电商网站(50+ 页面)

方案一:不分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████████████████████ 2.5MB
首屏时间:4.2 秒
LCP: 4.8 秒

方案二:路由分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████████ 800KB (-68%)
首屏时间:1.8 秒 (-57%)
LCP: 2.1 秒 (-56%)

方案三:精细分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████ 500KB (-80%)
首屏时间:1.2 秒 (-71%)
LCP: 1.5 秒 (-69%)

分包策略组合:
  ✓ 路由分包
  ✓ 第三方库拆分
  ✓ 组件懒加载
  ✓ 大文件单独处理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Chunk 大小分布

优化后的 Chunk 分布:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
framework.js    ████████ 150KB (Vue + Router + Pinia)
ui.js           ████████████ 220KB (Element Plus)
utils.js        ████ 80KB (Lodash + Dayjs)
charts.js       ██████████ 180KB (ECharts)
index.js        ██ 40KB (入口文件)
Home.js         ████ 70KB (首页)
About.js        ██ 30KB (关于页)
User.js         ███ 50KB (用户页)
...             其他页面

总计:~820KB (gzip 后 ~280KB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

七、常见问题解决

问题排查表

问题原因解决方案
分包太多manualChunks 过细合并相似的 chunk,设置合理阈值
分包太少未配置 dynamic import使用 () => import() 动态导入
Chunk 重复共享代码未提取使用 experimentalOptimizeMinimize
Hash 频繁变化共享代码包含业务逻辑抽离纯工具代码到独立 chunk
循环依赖模块间相互引用重构代码结构,打破循环

调试技巧

javascript
// 1. 查看分包结果
npm run build

// 2. 分析包体积
npm install -D rollup-plugin-visualizer

// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'stats.html',
      open: true,
      gzipSize: true
    })
  ]
})

// 3. 查看网络请求
Chrome DevTools → Network
筛选 JS 文件,查看加载顺序和大小

// 4. 检查依赖关系
npx madge --circular src/

八、面试标准回答

Vite 的分包主要依靠 Rollup 的代码分割机制,核心目的是减少初始加载体积,提升首屏速度。

分包的原理:Vite 在生产环境使用 Rollup 打包,Rollup 会自动分析代码中的静态 import 和动态 import()。对于动态导入的模块,Rollup 会自动创建独立的 chunk。我们也可以通过 manualChunks 配置手动控制分包策略。

常用的分包策略有四种

  1. 路由分包:使用动态导入 () => import() 让每个路由页面成为独立的 chunk
  2. 组件懒加载:对大型组件使用 defineAsyncComponent 异步加载
  3. 第三方库拆分:通过 manualChunks 将 node_modules 中的依赖按类型拆分
  4. 大文件单独拆分:对超过阈值或特定的大文件单独打包

配置示例

javascript
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        'vue-vendor': ['vue', 'vue-router', 'pinia'],
        'element-plus': ['element-plus'],
        'utils': ['lodash-es', 'dayjs']
      }
    }
  }
}

实际项目中,我会:

  • 首先使用路由分包,这是收益最大的
  • 然后将大型第三方库(如 ECharts)单独拆分
  • 对不常用的重型组件进行懒加载
  • 最后用 rollup-plugin-visualizer 分析包体积,进一步优化

效果:通常能将初始加载体积减少 70-80%,首屏时间提升 50-70%。


九、记忆口诀

Vite 分包歌诀:

分包目的要记清,
减少加载是核心。
路由分包最常用,
动态导入自动分!

第三方库要拆分,
manualChunks 来帮忙。
Vue 库 UI 和工具,
各自独立缓存优!

组件太大懒加载,
defineAsyncComponent。
大文件也单独拆,
性能提升很明显!

配置完成看效果,
visualizer 来分析。
初始加载降下来,
首屏速度提上去!

十、推荐资源


十一、总结一句话

Vite 分包: 动态导入 + manualChunks = 首屏加载快 70%

最近更新