分包是什么

“分包” 就是按 “使用时机” 和 “功能” 将代码分割成多个小文件,核心是 “按需加载”,解决传统单包模式下 “体积过大、加载慢” 的问题。

  • 路由分包、组件分包、第三方库分包是最常用的三种方式;
  • 实现上主要依赖 import() 动态导入语法和打包工具(Webpack/Vite)的配置;
  • 最终目标是让用户 “用什么加载什么”,提升页面打开速度和交互体验。

为什么分包能优化性能?

  1. 减少首屏加载时间:只加载必要代码,缩小初始下载体积;
  2. 利用浏览器缓存:第三方库、不常更新的代码被缓存,后续访问更快;
  3. 避免重复加载:多个页面共用的代码(如公共组件)可拆分成 “共享包”,加载一次后复用。

分包后的 “加载流程”:浏览器如何处理多个包?

  1. 首屏加载:浏览器下载主包(app.js)和当前页面必需的分包(如首页路由的 home.js);

  2. 解析执行:主包代码先执行,初始化应用(如创建 Vue 实例、配置路由);

  3. 按需加载:当用户触发某个操作(如跳转路由、点击按钮),需要新的分包时:

    • 浏览器通过 import() 动态请求对应的分包文件(如 order.js);
    • 下载完成后,执行分包代码并渲染新内容(过程中可显示 “加载中” 提示)。

分包后的三个基本方向

1. 路由分包(最常用):按页面拆分,访问时才加载对应页面代码

2. 组件分包:按组件拆分,用到时才加载大型组件

3. 第三方库分包:将大型依赖单独拆分,利用缓存

1. 路由分包

路由拆分的关键是修改 router/index.js 中 “路由组件的导入方式”,将静态 import 改为动态 () => import()

(1)未拆分的静态导入(反面示例)

所有页面代码会打包到一起,不推荐:

// router/index.js(未拆分,不推荐)
import Vue from 'vue';
import Router from 'vue-router';
// 静态导入所有路由页面(会全部打包到核心 JS)
import Home from '@/views/Home'; 
import About from '@/views/About';
import User from '@/views/User';

Vue.use(Router);

export default new Router({
  routes: [
    { path: '/', name: 'Home', component: Home },
    { path: '/about', name: 'About', component: About },
    { path: '/user', name: 'User', component: User }
  ]
});
(2)拆分后的动态导入(正确示例)

每个路由页面会被拆分为独立 Chunk:

// router/index.js(已拆分,推荐)
import Vue from 'vue';
import Router from 'vue-router';
// 无需静态导入页面,改为动态导入
import ElementUI from 'element-ui'; // 第三方 UI 库(会被拆到 chunk-vendors)
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(Router);
Vue.use(ElementUI);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      // 动态导入:Home 页面会被拆分为独立 Chunk
      component: () => import('@/views/Home') 
    },
    {
      path: '/about',
      name: 'About',
      // 可选:给 Chunk 自定义名称(打包后文件名更清晰)
      component: () => import(/* webpackChunkName: "about-page" */ '@/views/About')
    },
    {
      path: '/user',
      name: 'User',
      component: () => import('@/views/User')
    }
  ]
});

打包前后对比:

非按需引入:

137cb84f27a2797ebbd8967f67983a8b.jpg

按需引入:

a2cf5fe7ed90b6f84b46857077eef9fe.jpg

2. 组件分包

组件的分包拆分(即 “异步组件”)是前端性能优化的关键手段之一,核心是将 “非首屏必需、体积较大或按需加载的组件” 从主页面代码中分离,单独打包成独立文件,仅在组件被使用时才加载。

一、先明确:哪些组件需要分包拆分?

不是所有组件都需要拆分,以下三类组件是 “分包重点”:

  1. 体积大的组件:包含大量 DOM 结构、复杂逻辑(如数据可视化图表、富文本编辑器)或依赖第三方库(如 ECharts 图表组件),单组件体积超过 100KB 时建议拆分。
  2. 按需触发的组件:用户操作后才显示的组件(如弹窗、抽屉、下拉菜单、折叠面板),默认隐藏状态下无需加载。
  3. 低频率使用的组件:如 “帮助中心”“关于我们”“投诉反馈” 等入口对应的组件,用户很少点击,没必要随页面初始加载。

二、Vue 中组件分包的实现方式(Vue 2 和 Vue 3)

1. Vue 2 中的实现:动态导入注册组件

Vue 2 中通过 “动态 import + 组件注册” 实现分包,无需额外 API:

<!-- 页面组件:ProductDetail.vue(商品详情页) -->
<template>
  <div>
    <!-- 主内容:立即加载 -->
    <div class="product-basic">图片、标题、价格...</div>
    
    <!-- 按需加载的组件:点击按钮才显示 -->
    <el-button @click="showComment = true">查看评价</el-button>
    <comment-list v-if="showComment" /> <!-- 评价列表组件(需拆分) -->
  </div>
</template>

<script>
export default {
  components: {
    // 关键:动态导入组件,实现分包
    CommentList: () => import('@/components/CommentList.vue') 
  },
  data() {
    return {
      showComment: false // 控制组件显示,初始为 false(不加载)
    };
  }
};
</script>
  • 原理:() => import('路径') 告诉 Webpack/Vite:“这个组件不是必须的,打包时单独拆成一个文件”。
  • 加载时机:只有当 showComment 变为 true(用户点击按钮)时,浏览器才会请求 CommentList 对应的 JS/CSS 文件。

2. Vue 3 中的实现:defineAsyncComponent(更强大、更完善、给vue官方点)

Vue 3 提供了 defineAsyncComponent API,专门用于异步组件,支持加载状态、错误处理等高级配置:

<!-- 页面组件:ProductDetail.vue -->
<template>
  <div>
    <div class="product-basic">图片、标题、价格...</div>
    <el-button @click="showComment = true">查看评价</el-button>
    <CommentList v-if="showComment" />
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue';
// 导入加载中、加载失败的占位组件(可选)
import Loading from '@/components/Loading.vue';
import Error from '@/components/Error.vue';

// 关键:用 defineAsyncComponent 定义异步组件,实现分包
const CommentList = defineAsyncComponent({
  loader: () => import('@/components/CommentList.vue'), // 动态导入路径
  loadingComponent: Loading, // 组件加载过程中显示的占位符
  errorComponent: Error, // 组件加载失败时显示的内容
  delay: 200, // 延迟 200ms 显示 loading(避免一闪而过)
  timeout: 5000 // 5秒内未加载完成则视为失败
});

const showComment = ref(false);
</script>
  • 优势:相比 Vue 2 的简单动态导入,defineAsyncComponent 能处理加载状态(避免用户看到空白)和错误情况(如网络故障),体验更友好。

三、自定义分包名称与公共组件拆分

1. 自定义分包文件名(便于调试)

默认情况下,拆分的组件文件会以哈希值命名(如 123.js),可通过 Webpack 魔法注释自定义名称:

// Vue 2 中
components: {
  CommentList: () => import(/* webpackChunkName: "comment-list" */ '@/components/CommentList.vue')
}

// Vue 3 中
const CommentList = defineAsyncComponent({
  loader: () => import(/* webpackChunkName: "comment-list" */ '@/components/CommentList.vue')
});

打包后会生成 comment-list.xxxx.js,更易识别。

2. 多个异步组件合并拆分(避免文件过多)

如果多个小异步组件(如弹窗 A、弹窗 B)都依赖同一个工具函数,可通过 “统一 chunk 名称” 将它们合并打包:

// 弹窗 A 组件
const PopupA = () => import(/* webpackChunkName: "popups" */ '@/components/PopupA.vue');
// 弹窗 B 组件
const PopupB = () => import(/* webpackChunkName: "popups" */ '@/components/PopupB.vue');

打包后,PopupA 和 PopupB 会合并到 popups.xxxx.js 中,避免生成过多小文件(小文件过多会增加 HTTP 请求次数)。

3. 避免过度拆分(反优化)

  • 体积小于 30KB 的组件无需拆分(拆分后增加的 HTTP 请求成本可能超过体积优化收益)。
  • 首屏必需的组件(如导航栏、页脚)不能拆分(拆分会导致首屏显示延迟)。

四、如何验证组件是否拆分成功?

  1. 打包后查看产物:执行 npm run build,在 dist/js 目录中查找是否有组件对应的独立文件(如 comment-list.xxxx.js)。

  2. 浏览器 Network 面板

    • 打开页面,初始加载时观察 Network 中的 JS 文件,确认异步组件的文件未被加载。
    • 触发组件显示(如点击 “查看评价”),此时会看到浏览器新请求该组件的 JS/CSS 文件,说明拆分生效。

注意

组件的分包拆分是 “同一页面内的按需加载优化”,与路由拆分(不同页面的按需加载)形成互补。核心逻辑是:用动态导入让非必需组件 “延迟加载”,减少首屏代码体积。实现时需注意 “按需拆分”(只拆大组件、按需组件),避免过度拆分导致请求增多。

第三方库的拆分

  1. Vue CLI 官方文档 - 构建优化在 Vue CLI 官方文档的「构建优化」章节中提到,其内置的 Webpack 配置会自动拆分代码,具体包括:

    • 分离第三方库(如 vuevue-router 等)和应用代码,避免第三方库被重复打包。
    • 拆分公共代码(多页面应用中共享的代码),减少整体打包体积。

    文档中明确说明:Vue CLI 的默认配置已针对大多数应用做了优化,包括合理的代码拆分策略。

  2. Vue CLI 内置 Webpack 配置解析Vue CLI 通过 @vue/cli-service 封装了 Webpack 配置,其默认的 splitChunks 配置逻辑可通过以下方式验证:

    • 执行 vue inspect --plugin splitChunks 命令(在 Vue CLI 项目根目录),可查看内置的代码拆分配置。

    • 输出结果中会包含类似以下的核心配置(简化版):

      splitChunks: {
        chunks: 'all', // 对所有类型的 chunk(初始、异步、所有)进行拆分
        cacheGroups: {
          vendors: {
            name: 'chunk-vendors', // 第三方库拆分后的文件名
            test: /[/]node_modules[/]/, // 匹配 node_modules 中的第三方库
            priority: 10, // 优先级高于默认的 common 组
            chunks: 'initial' // 针对初始 chunk 拆分
          },
          common: {
            name: 'chunk-common', // 公共代码拆分后的文件名
            minChunks: 2, // 被至少 2 个 chunk 共享才会拆分
            priority: 1, // 优先级低于 vendors 组
            reuseExistingChunk: true // 复用已存在的 chunk
          }
        }
      }
      

      这一配置明确将 node_modules 中的第三方库(如 vueaxios 等)拆分为 chunk-vendors.js,而应用自身代码和公共组件拆分为其他 chunk,与官方描述一致。也就是所有的三方库为一个大的文件,其他的为一个文件这样的形式去打包

如果对三方库各自进行打包?

假设项目有两个独立业务模块:

  • 数据可视化模块:依赖 echartschart.js
  • 文档处理模块:依赖 xlsxpdfjs-dist

默认分包会把这 4 个库全部混入 chunk-vendors.js,如果用户只访问 “数据可视化模块”,xlsx 和 pdfjs-dist 的代码就是 “无效加载”;且只要其中一个库更新(如 echarts 升级),整个 chunk-vendors.js 的 hash 会变,导致所有依赖这个包的页面缓存失效。

手动分库解决:

按业务模块拆分第三方库,让每个模块的依赖独立打包:

// vue.config.js
module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        cacheGroups: {
          // 1. 数据可视化模块的第三方库
          vendor-visual: {
            test: /[/]node_modules[/](echarts|chart.js)[/]/,
            name: 'chunk-vendor-visual', // 独立包:仅包含可视化相关库
            priority: 20,
            chunks: chunk => chunk.name.includes('visual') // 仅对“可视化模块页面”生效
          },
          // 2. 文档处理模块的第三方库
          vendor-doc: {
            test: /[/]node_modules[/](xlsx|pdfjs-dist)[/]/,
            name: 'chunk-vendor-doc', // 独立包:仅包含文档相关库
            priority: 20,
            chunks: chunk => chunk.name.includes('doc') // 仅对“文档模块页面”生效
          },
          // 3. 通用核心库(vue、vue-router 等)
          vendors: {
            test: /[/]node_modules[/]/,
            name: 'chunk-vendors',
            priority: 10,
            // 排除上述两个业务模块的依赖
            exclude: /[/]node_modules[/](echarts|chart.js|xlsx|pdfjs-dist)[/]/
          }
        }
      }
    }
  }
};

结果:

  • 用户访问 “可视化模块” 时,仅加载 chunk-vendors.js + chunk-vendor-visual.js,无无效代码;
  • 当 echarts 升级时,仅 chunk-vendor-visual.js 的 hash 变化,chunk-vendor-doc.js 和通用 chunk-vendors.js 的缓存不受影响,提升后续访问速度。
  1. 第三方库的打包是按需引入好还是全局引入好

先明确两种引用方式的打包差异

不管是 Vue CLI 还是 Vite,对 Vant UI 的打包处理逻辑都和 “引用范围” 强相关,先理清本质差异:

引用方式打包结果核心逻辑
全局引用所有 Vant 组件(即使没用到)都打包进 chunk-vendors.js(或类似第三方库 chunk),最终只有 1 个第三方库文件全局注册时,Webpack/Vite 会把整个 vant 包视为 “必需依赖”,无法 Tree-Shaking 剔除未使用组件
按需引用只打包你实际用到的 Vant 组件(如 ButtonDialog),每个组件(或组件组)可能拆成独立小 chunk(如 chunk-vant-button.js),最终会多几个小文件按需引入时(如 import Button from 'vant/lib/button' 或用 Vant 插件),工具能精准识别 “用到的代码”,未使用组件被 Tree-Shaking 剔除,同时按组件拆分 chunk

1. 优先选 “全局引用” 的场景

  • 小项目 / 工具类项目:如内部管理后台、简单的活动页,用到的 Vant 组件少(或几乎全用),且对首屏加载速度要求不高(用户多为内部人员,网络环境稳定)。
  • 快速迭代 / 原型开发:需要快速出效果,不想在 “组件引入” 上花时间,优先保证开发效率。

2. 优先选 “按需引用” 的场景

  • 首屏优化敏感项目:如 C 端用户产品(电商、社交 App 前端),首屏加载速度直接影响用户留存,需要极致减小首屏资源体积(LCP 指标要求 ≤2.5s)。
  • 只用到少量 Vant 组件:如项目只需要 Vant 的 ButtonToastDialog 3 个组件,按需引用能避免打包 150KB+ 的全量包,体积优势明显。
  • 用 HTTP/2 部署:现代服务器基本支持 HTTP/2,多路复用能并行处理多请求,“多文件” 的请求成本几乎可以忽略,按需引用的 “体积小” 优势被放大。

分包一定好吗

Vue CLI 默认会对 “体积超过 30KB(压缩前)” 的依赖单独拆分,但有时会出现两种问题:

  1. 小库过多:多个体积很小的依赖(如 lodash-es 的子模块、date-fns)被拆分成多个小 chunk,导致浏览器请求数增加(HTTP/1.1 环境下会阻塞加载);
  2. 重复依赖:不同业务包中重复引入了同一依赖(如 lodash 的 debounce 方法),默认未合并,导致代码冗余。

手动分库解决:

  • 合并小库:将多个小体积依赖合并到一个 chunk,减少请求数;
  • 提取重复依赖:将重复引入的依赖单独拆分,实现复用。
// vue.config.js
module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        minSize: 10000, // 调整最小分包体积(如 10KB 以下不单独拆分)
        cacheGroups: {
          // 合并小体积工具库(lodash-es、date-fns 等)
          vendor-utils: {
            test: /[/]node_modules[/](lodash-es|date-fns|dayjs)[/]/,
            name: 'chunk-vendor-utils', // 合并成一个工具库包
            priority: 20,
            minSize: 0, // 强制合并,忽略 minSize 限制
            minChunks: 2 // 被引用超过 2 次才拆分(避免单次引用的小库被合并)
          }
        }
      }
    }
  }
};

手动分库打包的核心判断标准(常规情况下)

当满足以下任一条件时,就需要手动干预 Vue CLI 的第三方库分包:

  1. 首屏 vendor 包体积过大(如超过 1MB),导致首屏加载慢;
  2. 第三方库按业务模块划分明确,需要拆分以优化缓存;
  3. 存在非标准依赖(私有库、CDN 依赖),默认分包未覆盖;
  4. 默认分包粒度不合理(小库过多导致请求数增加,或重复依赖导致冗余)。

简单说:Vue CLI 的默认分包是 “通用方案”,当项目有个性化的性能优化需求特殊依赖场景时,就需要手动配置 splitChunks 来调整分库逻辑。

总的来说分包常规配置就够用啦,无特殊需求千万别动,代码能跑就是好代码

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]