FORMA

编译与打包优化

构建阶段的代码分割、Tree Shaking 与 SFC 编译优化。运行时见 runtime

在前端工程化中,性能优化不仅局限于运行时,更应前置到编译和打包阶段。通过合理的构建配置、代码分割、Tree Shaking 及 Vue 特有的编译优化,可以显著减少产物体积、提升加载速度及运行时效率,本节将围绕这些核心点展开。

一、异步组件与懒加载

懒加载可将应用拆分成更小的代码块,仅在需要时才加载,有效减少首屏资源体积。

1. defineAsyncComponent 的完整配置

Vue 3 提供了 defineAsyncComponent 用于定义异步组件,支持丰富的配置选项(加载成功/加载中/加载失败的状态展示,以及超时控制)。

js
import { defineAsyncComponent } from "vue";

const AsyncComponent = defineAsyncComponent({
  // 实际加载组件函数(必须返回 Promise)
  loader: () => import("./HeavyComponent.vue"),

  // 加载过程中展示的组件(可选)
  loadingComponent: LoadingComponent,

  // 加载失败时展示的组件(可选)
  errorComponent: ErrorComponent,

  // 展示 loadingComponent 之前的延迟(默认 200ms,防止闪烁)
  delay: 200,

  // 超时时间(毫秒),超时后显示 errorComponent
  timeout: 3000,

  // 是否支持 Suspense(默认 true)
  suspensible: false,

  // 服务端渲染时启用
  serverPrefetch: true,
});

使用场景

  • 首屏不可见的复杂图表组件、表格组件。
  • 根据用户交互才触发的弹窗、表单等。
  • 需要 fallback 状态(如加载中或错误重试)的组件。

2. 路由级懒加载 vs 组件级懒加载的取舍

层级方式优点缺点
路由级在路由配置中使用 () => import('@/views/Home.vue')粒度大,简单高效,首屏只加载当前路由组件组件较小时可能产生过多小 chunk,增加请求数
组件级在组件内部使用 defineAsyncComponent 动态引入子组件更精细的控制(如弹窗、折叠面板等按需加载),减少初始加载需手动管理,引入一定复杂度

取舍原则

  • 默认使用路由级懒加载,将每个页面路由打包为独立 chunk。
  • 对于页面内用户可能要过一段时间才看到的区域(如底部评论区、模态框、标签页中的隐藏面板),使用组件级懒加载。
  • 对于稳定高频的核心组件(如导航栏、侧边栏)则不应该懒加载,应同步加载。

二、Tree Shaking

Tree Shaking 是指构建工具(Webpack、Vite)通过静态分析,剔除未被使用的代码。

1. Vue 3 的按需引入设计

Vue 3 的核心 API(如 refreactivecomputedwatch 等)都采用 ESM 命名导出,且无副作用(side effect free)。这意味着构建工具可以安全地“摇掉”未使用的 API。

js
// 只引入 ref 和 computed,未使用的 reactive 不会被打包
import { ref, computed } from "vue";

对比 Vue 2 全局 Vue 对象,Tree Shaking 非常困难。Vue 3 的做法大幅减少了打包体积。

2. 确保第三方库支持 Tree Shaking

  • 使用 ES 模块版本(如 lodash-es 替代 lodash)。
  • 检查 package.json 中是否包含 sideEffects: false 或明确声明。
  • 配置构建工具保留模块结构(如 Vite 默认支持,Webpack 需 mode: 'production')。

3. SFC 中的 Tree Shaking

Vue 单文件组件(SFC)中的 <template><style> 也会参与 Tree Shaking:如果整个组件未被引用,则其所有依赖均不会打包。

三、Vue 单文件组件的编译优化

Vue 3 的编译器(@vue/compiler-dom)在编译 SFC 时会执行多项优化,减少运行时代码量和 Diff 压力。

1. 静态标记(PatchFlag)

在编译模板时,编译器会为动态节点添加一个 PatchFlag(数字标记),指示该节点在更新时可能发生哪些变化(文本、属性、类、样式等)。

html
<div :class="activeClass" :id="dynamicId">{{ message }}</div>

编译后大致生成:

js
_createVNode(
  "div",
  {
    class: _normalizeClass(activeClass),
    id: dynamicId,
  },
  _toDisplayString(message),
  3 /* TEXT + CLASS + PROPS */,
);

运行时 Diff:渲染器根据 PatchFlag 仅检查对应类型的变更,而不需要深度对比整个 VNode 树。例如 TEXT 标记意味着只需对比文本内容,CLASS 标记只需对比 class 属性。

优势:跳过静态子树的 Diff(静态提升),大幅提升组件更新性能。

2. 事件缓存

Vue 3 编译器会自动缓存模板中的内联事件处理函数,避免每次渲染都创建新函数。

html
<button @click="count++">Increment</button>

传统做法:每次重新渲染都会生成一个新的箭头函数,导致子组件不必要的重新渲染(如果子组件通过 v-on 监听)。Vue 3 编译器将其转换为:

js
_createVNode("button", { onClick: _cache[0] || (_cache[0] = ($event) => count.value++) });

这样 onClick 始终保持同一个函数引用,有利于优化子组件的 shouldUpdate

3. 静态提升(Static Hoisting)

编译器会将纯静态的节点提升到渲染函数外部,避免每次渲染都重新创建。

html
<div class="static">I never change</div>

提升后:

js
const _hoisted_1 = _createVNode("div", { class: "static" }, "I never change", -1 /* HOISTED */);
function render() {
  return _hoisted_1;
}

多个静态相邻元素会被合并为一个 VNode(_createStaticVNode),进一步减少内存占用。

4. 块树优化(Block Tree)

对于带有 v-ifv-for 的结构,Vue 3 引入 Block 概念,收集动态节点,使 Diff 直接定位到动态部分,无需遍历整个树。

四、构建工具配置优化

以 Vite 为例(同样适用于 Webpack),通过合理配置可以进一步优化生产产物。

1. Vite 预构建(Pre-bundling)

Vite 在开发环境下使用 esbuild 预构建依赖(如 vueelement-plus 等),将 CommonJS 或 UMD 转换成 ESM,并合并多个内部模块,减少浏览器请求数量。

生产环境:Vite 使用 Rollup 进行打包,同样可以对依赖提前优化,通过 optimizeDeps 选项控制预构建行为。

js
// vite.config.js
export default {
  optimizeDeps: {
    include: ["lodash-es", "axios"], // 强制预构建
    exclude: ["your-local-lib"], // 排除无需预构建的库
  },
};

2. 代码压缩(Minify)

生产环境必须开启代码压缩,Vite 默认使用 esbuild(极快)或 terser(更兼容,但慢)。

js
build: {
  minify: 'esbuild',   // 或 'terser'
  terserOptions: { compress: { drop_console: true } } // 移除 console
}

对于大型项目,建议使用 esbuild 压缩以获得更快速度,同时开启 target 降低语法转化级别。

3. CDN 外部化(Externals)

将一些大型库(如 vueelement-plusecharts)通过 CDN 引入,不打包到 bundle 中,利用浏览器缓存和 CDN 加速。

Vite 配置

js
import { defineConfig } from "vite";
export default defineConfig({
  build: {
    rollupOptions: {
      external: ["vue", "element-plus"],
      output: {
        globals: {
          vue: "Vue",
          "element-plus": "ElementPlus",
        },
      },
    },
  },
});

同时需要在 index.html 中通过 <script> 标签引入这些库。注意:外部化会增加依赖管理复杂度,推荐仅对稳定、大体积、跨项目共享的库使用。

4. 分块策略(Chunk Split)

合理分割第三方库和业务代码,避免单个 chunk 过大。

Rollup / Vite 配置

js
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        if (id.includes('node_modules')) {
          if (id.includes('vue')) return 'vendor-vue';
          if (id.includes('element-plus')) return 'vendor-element';
          return 'vendor';
        }
      }
    }
  }
}

通过手动分块,将 vueelement-plus 等单独打包,用户缓存效果更好。

五、工程级优化总结

优化类别关键技术点收益
异步组件与懒加载defineAsyncComponent 完整配置;路由级懒加载 + 组件级懒加载组合减少首屏 JS 体积,提升加载速度
Tree Shaking使用 ESM 导入 API;检查 sideEffects;构建工具开启生产优化移除未使用代码,减少最终 bundle 体积
SFC 编译优化静态标记(PatchFlag)、事件缓存、静态提升、块树优化提升运行时渲染性能,减少 Diff 开销
构建工具配置Vite 预构建;代码压缩(esbuild/terser);CDN 外部化;分块策略更快的构建速度,更小的产物,更好的缓存

工程级性能优化需要贯穿开发、构建、部署全流程。Vue 3 + Vite 的组合提供了开箱即用的优秀优化,而深入理解上述机制,可以在遇到性能瓶颈时更有针对性地调优。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:性能官方最佳实践
Vue:异步组件defineAsyncComponent
Vite:构建优化生产构建

Series

performance

2 / 3