CC 咖啡猫的工作空间 Coding Space

构建优化实践(Vite + Webpack)

1. 构建工具选型对比

1.1 主流构建工具概览

工具 语言 底层 开发模式 生产模式 生态
Vite TypeScript esbuild (预构建) + Rollup (打包) 原生 ESM 按需编译 Rollup 打包 + 插件 丰富,兼容 Rollup 插件
Webpack JavaScript JavaScript + Node.js 全量打包 + HMR 全量打包 + 优化 最丰富,成熟稳定
Turbopack Rust Next.js 定制 增量计算 + 函数级缓存 开发模式专用 仅 Next.js 生态
Rspack Rust 自研(兼容 Webpack 插件) Rust 原生并行编译 Rust 原生并行打包 兼容 Webpack loader/plugin

1.2 开发体验对比(HMR 速度)

场景 Vite Webpack 5 Rspack Turbopack
冷启动(1000 模块) ~300ms(esbuild 预构建) ~10s+ ~500ms ~200ms
热更新(单文件改动) <50ms(ESM 按需更新) ~200ms-1s(重建 chunk) <100ms <50ms
热更新(大文件编辑) 几乎不受影响 随模块数线性增长 受轻微影响 几乎不受影响
首次页面加载 需要额外 ESM 请求 直接加载打包后文件 直接加载 分段加载
// Vite 开发模式原理:利用浏览器原生 ESM,无需打包
// 源文件 → esbuild 转译 → 浏览器直接 import
// 修改文件 → 仅发送被修改的模块 → 浏览器自行更新依赖图

// Webpack 开发模式原理:打包为 bundle 后推送
// 源文件 → 全量打包 → bundle 推送到内存 → WebSocket 通知更新
// 修改文件 → 重新打包该 module → 发送增量 patch → eval 执行

1.3 生产构建速度对比

指标 Vite (Rollup) Webpack 5 Rspack Turbopack
中型项目(~500 模块) ~8s ~25s ~5s 仅开发模式
大型项目(~3000 模块) ~30s ~90s+ ~15s 仅开发模式
产物体积优化 优秀(Rollup 天然 Tree-shaking) 良好(需配置) 良好 N/A
代码分割能力 灵活(manualChunks) 丰富(SplitChunks) 兼容 Webpack N/A

1.4 选型建议

项目类型 推荐工具 理由
新项目(Vue/React) Vite 开箱即用,HMR 极快,配置简单
遗留 Webpack 项目迁移 Rspack 兼容 Webpack 配置,迁移成本低,构建快 5-10x
Next.js 项目 Turbopack 框架内置,开发模式优化
需要 Webpack 特有插件/loader Webpack 5 生态最成熟,兼容性最好
Monorepo 大型项目 Vite + Rspack Vite 写配置,Rspack 做生产构建

2. Vite 构建优化

2.1 预构建依赖优化(optimizeDeps)

原理:Vite 在开发模式下使用 esbuild 对 node_modules 中的依赖进行预构建,将 CommonJS/UMD 模块转为 ESM,并合并分散的依赖文件以减少 HTTP 请求数。

配置示例

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    // 明确指定需要预构建的依赖(默认自动检测)
    include: [
      'lodash-es',
      'dayjs',
      'axios',
    ],
    // 排除不需要预构建的依赖(已为 ESM 的库)
    exclude: [
      '@vueuse/core',
      'pinia',
    ],
    // 当依赖项有动态导入或 require 时,强制预构建
    force: true, // 开发中调试依赖时使用
  },
})

注意事项/踩坑点

1. 某些 ESM 包如果包含动态 import,需要手工加入 include
2. 修改 optimizeDeps 后需重启 dev server 才能生效
3. force: true 会清除缓存,不要在生产构建中使用
4. 遇到 "Outdated optimisticDeps" 警告时,清空 node_modules/.vite

好的实践:明确声明项目直接依赖

// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: [
      // 将分散的模块合并为一个请求
      'echarts/core',
      'echarts/charts',
      'echarts/components',
    ],
  },
})

不好的实践:不合理的 exclude

// ❌ 误将包含 require() 的 CJS 库排除
export default defineConfig({
  optimizeDeps: {
    exclude: ['lodash'], // lodash 是 CJS,排除后浏览器无法运行
  },
})

2.2 代码分割(Code Splitting)

原理:将代码拆分为多个 chunk,按需加载,减少首屏加载体积。

路由懒加载

Vue 3 实现(defineAsyncComponent + Vue Router)

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { defineAsyncComponent } from 'vue'
import { ElLoading } from 'element-plus'

// 方式一:使用 defineAsyncComponent(推荐)
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: defineAsyncComponent({
      loader: () => import('@/views/Dashboard/index.vue'),
      loadingComponent: () => <ElLoading />, // 加载中组件
      errorComponent: () => <div>加载失败</div>, // 错误组件
      delay: 200, // 延迟显示 loading(毫秒)
      timeout: 10000, // 超时时间
    }),
  },
  {
    path: '/users',
    name: 'Users',
    // 方式二:直接使用动态 import(Vite 自动分割)
    component: () => import('@/views/Users/index.vue'),
  },
]

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

export default router

React 实现(lazy + Suspense)

// src/router/index.tsx
import { lazy, Suspense } from 'react'
import { createBrowserRouter } from 'react-router-dom'
import { Spin } from 'antd'

// 路由懒加载组件
const Dashboard = lazy(() => import('@/views/Dashboard'))
const Users = lazy(() => import('@/views/Users'))

// 加载中组件
const PageLoading = () => (
  <div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
    <Spin size="large" />
  </div>
)

const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: (
      <Suspense fallback={<PageLoading />}>
        <Dashboard />
      </Suspense>
    ),
  },
  {
    path: '/users',
    element: (
      <Suspense fallback={<PageLoading />}>
        <Users />
      </Suspense>
    ),
  },
])

export default router

2.3 manualChunks 分包策略

原理:在 Vite 的生产构建阶段(Rollup),通过 build.rollupOptions.output.manualChunks 自定义 chunk 分割规则。

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 分包策略
        manualChunks: {
          // 1. Vue 生态全家桶
          'vue-vendor': [
            'vue',
            'vue-router',
            'pinia',
            'vue-i18n',
          ],
          // 2. React 生态全家桶
          'react-vendor': [
            'react',
            'react-dom',
            'react-router-dom',
            'zustand',
          ],
          // 3. UI 组件库(单独分包,避免与业务代码混装)
          'ui-element': ['element-plus'],
          'ui-antd': ['antd', '@ant-design/icons'],
          'ui-vxe': ['vxe-table', 'vxe-pc-ui'],
          // 4. 工具库
          'utils-vendor': [
            'lodash-es',
            'dayjs',
            'axios',
            'zxcvbn',
          ],
          // 5. 图表库
          'chart-vendor': ['echarts', 'vue-echarts'],
        },
      },
    },
    // CSS 提取配置
    cssCodeSplit: false, // 合并所有 CSS 为一个文件
  },
})
分包策略 优点 缺点 适用场景
vendor 分包 第三方库变更少,缓存利用率高 首次加载 vendor 体积大 所有项目
按功能分包 按业务模块拆分,按需加载 模块间依赖复杂时可能重复 大型项目
按 UI 库分包 Element Plus / Ant Design 独立 多个 UI 库时增多请求数 多 UI 库项目
按页面分包 结合路由懒加载,首屏最优 公共依赖可能重复打包 页面级应用

更好的分包策略:动态检测

// vite.config.ts
import { defineConfig } from 'vite'

// 自动检测 node_modules 中的依赖大小,按规则分包
function createManualChunks() {
  const vendorGroups: Record<string, RegExp[]> = {
    'vue-vendor': [/node_modules\/vue/, /node_modules\/@vue/],
    'react-vendor': [/node_modules\/react/],
    'ui-element': [/node_modules\/element-plus/, /node_modules\/@element-plus/],
    'ui-antd': [/node_modules\/antd/, /node_modules\/@ant-design/],
    'utils': [/node_modules\/lodash/, /node_modules\/dayjs/, /node_modules\/axios/],
    'echarts': [/node_modules\/echarts/, /node_modules\/zrender/],
  }

  return (id: string) => {
    for (const [name, patterns] of Object.entries(vendorGroups)) {
      if (patterns.some((p) => p.test(id))) {
        return name
      }
    }
    // 其他 node_modules 归入 vendor
    if (id.includes('node_modules')) {
      return 'vendor'
    }
  }
}

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: createManualChunks(),
      },
    },
  },
})

不好的实践

// ❌ 未配置 manualChunks,所有代码打包到同一个 JS 文件
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 没有 manualChunks 配置 → 单文件超大 bundle
      },
    },
  },
})

// ❌ 过度分包导致请求数爆炸
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 每个 node_modules 包都独立为一个 chunk
          if (id.includes('node_modules')) {
            return id.split('/node_modules/')[1].split('/')[0]
          }
        },
      },
    },
  },
})

2.4 CSS 提取与压缩

配置示例

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    // CSS 代码分割(默认 true,每个路由一个 CSS 文件)
    cssCodeSplit: true,
    // CSS 压缩
    cssMinify: 'esbuild', // 'esbuild' | 'lightningcss' | boolean
    // 生成 sourcemap
    sourcemap: false,
    // 目标浏览器
    target: 'es2015',
  },
  // CSS 预处理配置
  css: {
    preprocessorOptions: {
      scss: {
        // 注入全局变量和 mixin,避免每个文件手动 @import
        additionalData: `@use "@/styles/variables" as *;`,
        api: 'modern-compiler', // 使用 Dart Sass
      },
    },
    // CSS modules 配置
    modules: {
      localsConvention: 'camelCaseOnly',
    },
    // PostCSS 配置(自动添加浏览器前缀等)
    postcss: {
      plugins: [
        require('autoprefixer')({
          overrideBrowserslist: ['last 2 versions', '> 1%', 'not dead'],
        }),
      ],
    },
  },
})
配置项 取值 说明
cssMinify 'esbuild' 默认,esbuild 快速压缩
cssMinify 'lightningcss' 更快,支持更现代化的 CSS 语法
cssMinify false 不压缩,调试用
cssCodeSplit true 开启 CSS 代码分割,按 async chunk 拆分

2.5 静态资源处理

原理:Vite 在构建时会自动处理静态资源,小于 assetsInlineLimit 的文件会被内联为 Base64,减少 HTTP 请求。

// vite.config.ts
import { defineConfig } from 'vite'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'

export default defineConfig({
  build: {
    // 静态资源输出目录
    assetsDir: 'assets',
    // 内联阈值(字节),小于此值的资源转为 Base64
    assetsInlineLimit: 4096, // 4KB
  },
  plugins: [
    // 图片压缩插件(生产构建时自动压缩图片)
    ViteImageOptimizer({
      png: { quality: 80 },
      jpeg: { quality: 80 },
      webp: { quality: 75 },
      avif: { quality: 60 },
    }),
  ],
})
资源类型 Vite 处理方式 优化建议
图片(png/jpg/gif/svg) 小于阈值内联,否则复制到 assetsDir 使用 WebP/AVIF 格式,图片压缩
字体(woff/woff2) 复制到 assetsDir 仅保留需要的字重,使用 font-display: swap
视频 复制到 assetsDir 使用 CDN 托管,避免打包
JSON/YAML 转为 ES module 按需 import,避免整体加载

2.6 Gzip / Brotli 压缩

原理:在构建时生成 .gz / .br 文件,配合 Nginx 等服务器在传输时启用压缩,减少网络传输体积。

// vite.config.ts
import { defineConfig } from 'vite'
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    viteCompression({
      // 压缩算法
      algorithm: 'brotliCompress', // 'gzip' | 'brotliCompress' | 'deflate'
      // 匹配的文件类型
      ext: '.br', // gzip 对应 '.gz'
      // 只压缩超过此大小的文件(字节)
      threshold: 1024, // 1KB
      // 删除源文件,只保留压缩文件(需服务器配置 Content-Encoding)
      deleteOriginFile: false,
      // 压缩质量(brotli: 0-11, gzip: 0-9)
      compressionOptions: {
        level: 9,
      },
    }),
  ],
})
# Nginx 配置示例
server {
    # 启用 Brotli 压缩(需安装 brotli 模块)
    brotli on;
    brotli_types text/plain text/css application/json application/javascript;
    brotli_comp_level 6;

    # 启用 Gzip 压缩(兜底)
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    gzip_comp_level 6;
    gzip_min_length 1000;
}

2.7 Tree Shaking 原理与保障

原理:Tree Shaking 通过静态分析 ES Module 的 import/export 语法,移除未被引用的代码。

// Vite 的 Tree Shaking 由 Rollup 在生产构建时完成
// 需要满足的条件:

// ✅ 可 Tree Shaking - 使用 ESM 语法
import { debounce } from 'lodash-es'     // 只引入 debounce 函数
import { ref, computed } from 'vue'      // 按需引入 Vue API

// ❌ 不可 Tree Shaking - CommonJS 语法
const _ = require('lodash')              // 全量引入,无法 tree-shake
import * as lodash from 'lodash'          // 全量引入
条件 说明
ES Module 必须使用 import/export 语法
sideEffects package.json 中声明 "sideEffects": false
纯函数调用 顶层调用的副作用函数不会被移除
动态引入 import() 按需加载天然支持

保障 Tree Shaking 的配置

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      // 明确告诉 Rollup 哪些模块无副作用
      treeshake: {
        moduleSideEffects: (id) => {
          // 排除 CSS 和 CSS modules
          if (id.endsWith('.css') || id.endsWith('.module.css')) {
            return true
          }
          // polyfill 保留副作用
          if (id.includes('core-js')) {
            return true
          }
          return false
        },
        // 预设属性值:'smallest' | 'safest' | 'recommended'
        preset: 'recommended',
      },
    },
  },
})

注意事项/踩坑点

1. lodash(CJS)无法 Tree Shaking,必须使用 lodash-es
2. 某些 UI 库的全局 CSS 引入会阻止 Tree Shaking
3. sideEffects: false 声明不当可能导致样式丢失
4. Babel 转译时可能破坏 ESM 语法,使用 @babel/preset-env 的 modules: false

3. Webpack 构建优化(对比 / 迁移场景)

3.1 优化 Loader 配置

// webpack.config.js(或 webpack.config.ts)
const path = require('path')

module.exports = {
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 启用 babel 缓存
            },
          },
        ],
        // ✅ 限制处理范围
        include: [path.resolve(__dirname, 'src')],
        exclude: [/node_modules/, /dist/],
      },
      {
        test: /\.svg$/,
        // ✅ 使用资源模块替代 url-loader/file-loader
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024, // 4KB
          },
        },
      },
    ],
  },
}
优化点 说明 效果
include 限定 loader 只处理 src 目录 减少文件扫描范围
exclude 排除 node_modules 避免重复处理第三方库
cacheDirectory 开启缓存 增量编译提速 2-5x
type: 'asset' Webpack 5 内置资源模块 替代 url-loader/file-loader

3.2 thread-loader 多线程

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: [
          'thread-loader', // 多线程(放在最前面)
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
            },
          },
        ],
        include: [path.resolve(__dirname, 'src')],
      },
    ],
  },
}
配置项 说明 建议值
workers 线程数 CPU 核心数 - 1(默认)
workerParallelJobs 每个线程的任务数 20(默认)
poolTimeout 线程池闲置超时 2000ms(默认)

注意事项

1. thread-loader 有线程启动开销,小型项目可能反而更慢
2. 不能与 mini-css-extract-plugin 配合使用
3. 在 Vue SFC 中慎用,可能引起编译问题
4. 仅对 CPU 密集型 loader(babel-loader, ts-loader)有效

3.3 cache 缓存

// webpack.config.js
module.exports = {
  // Webpack 5 内置缓存
  cache: {
    type: 'filesystem', // 'memory' | 'filesystem'
    cacheDirectory: path.resolve(__dirname, '.temp_cache'),
    buildDependencies: {
      // 配置文件变了则清除缓存
      config: [__filename],
    },
    // 缓存版本,升级依赖后需手动更新
    version: '1.0',
    // 缓存内存存储限制(filesystem 也经过内存)
    maxMemoryGenerations: 1,
    // 按模块类型缓存
    profile: true,
  },
}
缓存类型 优点 缺点
memory 无磁盘 I/O,快速 进程退出后丢失
filesystem 跨进程共享,持久化 首次构建需写入缓存

3.4 SplitChunksPlugin 分包

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // 'initial' | 'async' | 'all'
      // 包的最小体积(字节),小于此值不拆分
      minSize: 20000, // 20KB
      // 包的最大体积,超过会进一步拆分
      maxSize: 244000, // 约 240KB
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        // 1. Vue 全家桶
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|pinia|vue-i18n)[\\/]/,
          name: 'vendor-vue',
          priority: 20,
        },
        // 2. React 全家桶
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom|zustand)[\\/]/,
          name: 'vendor-react',
          priority: 20,
        },
        // 3. UI 库
        ui: {
          test: /[\\/]node_modules[\\/](element-plus|antd|@ant-design|vxe-table)[\\/]/,
          name: 'vendor-ui',
          priority: 15,
        },
        // 4. 第三方库
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10,
        },
        // 5. 公共代码(被多个 chunk 引用)
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

3.5 Webpack 迁移到 Vite 对比

Webpack 配置 Vite 等效配置 说明
babel-loader 内置 esbuild Vite 使用 esbuild 转译,速度 10-20x
ts-loader 内置 esbuild 仅转译,不做类型检查
url-loader/file-loader assetsInlineLimit Vite 内置资源处理
css-loader/style-loader 内置 CSS 处理 Vite 天然支持 CSS
MiniCssExtractPlugin build.cssCodeSplit Vite 默认提取 CSS
TerserPlugin build.minify: 'esbuild' 默认使用 esbuild 压缩
SplitChunksPlugin manualChunks 等效功能
DefinePlugin define 语法略有不同

4. 包体积分析

4.1 rollup-plugin-visualizer(Vite)

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

export default defineConfig({
  plugins: [
    visualizer({
      // 输出分析报告
      filename: 'dist/stats.html',
      // 图表模板
      template: 'treemap', // 'treemap' | 'sunburst' | 'network'
      // 打开分析页面
      open: true,
      // 显示 gzip 后大小
      gzipSize: true,
      // 显示 brotli 后大小
      brotliSize: true,
    }),
  ],
})

4.2 webpack-bundle-analyzer(Webpack)

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'dist/stats.html',
      openAnalyzer: true,
      generateStatsFile: true,
      statsFilename: 'dist/stats.json',
    }),
  ],
}

4.3 如何分析并优化大依赖

步骤 方法 说明
1. 生成分析报告 visualizer / bundle-analyzer 可视化查看各模块大小
2. 定位大依赖 按大小排序,关注 >100KB 的模块 查找意外引入的依赖
3. 检查重复打包 分析是否有多个版本的同个库 使用 resolve.alias 统一版本
4. 评估必要性 是否只有部分功能被使用 考虑按需导入或替换
5. 制定优化方案 替换、按需、分包 针对性处理

分析优化实战

# 1. 构建并生成分析报告
pnpm build

# 2. 查看 dist/stats.html
# 发现 moment.js 贡献了 600KB+,但只用了 format() 和 locale()
// 3. 制定优化方案

// ❌ 优化前:moment.js 全量打包(~600KB)
import moment from 'moment'
moment.locale('zh-cn')
console.log(moment().format('YYYY-MM-DD'))

// ✅ 优化后:替换为 dayjs(~6KB)
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
console.log(dayjs().format('YYYY-MM-DD'))

5. 构建产物优化

5.1 路由懒加载(已在上文详述)

5.2 组件级懒加载

Vue 3 实现

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'

// 组件级懒加载:仅在需要时加载
const HeavyChart = defineAsyncComponent(() => import('@/components/HeavyChart.vue'))

// 条件渲染时加载(弹框、Tab 切换)
const showChart = ref(false)

// 预加载策略:hover 时提前加载
function preloadChart() {
  const preloader = import('@/components/HeavyChart.vue')
}
</script>

<template>
  <div>
    <el-button @click="showChart = true" @mouseenter="preloadChart">
      显示图表
    </el-button>

    <!-- 懒加载组件仅在 v-if 为 true 时才开始加载 -->
    <HeavyChart v-if="showChart" />
  </div>
</template>

React 实现

import { useState, lazy, Suspense } from 'react'

// 组件级懒加载
const HeavyChart = lazy(() => import('@/components/HeavyChart'))

function Dashboard() {
  const [showChart, setShowChart] = useState(false)

  // 预加载策略
  const preloadChart = () => {
    import('@/components/HeavyChart')
  }

  return (
    <div>
      <button
        onClick={() => setShowChart(true)}
        onMouseEnter={preloadChart}
      >
        显示图表
      </button>

      {showChart && (
        <Suspense fallback={<div>加载中...</div>}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  )
}

5.3 第三方库按需导入

Element Plus(Vue 3)

// vite.config.ts
import { defineConfig } from 'vite'
import ElementPlus from 'unplugin-element-plus/vite'

export default defineConfig({
  plugins: [
    ElementPlus({
      // 配置按需导入
      useSource: true,
      // 图标按需导入
      icon: 'ElIcon',
    }),
  ],
})
// 使用方式(自动按需导入,无需手动 import)
// ❌ 不需要这样
// import { ElButton } from 'element-plus'

// ✅ 直接使用,插件自动按需导入
<template>
  <el-button type="primary">提交</el-button>
</template>

Ant Design(React)

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

export default defineConfig({
  plugins: [react()],
  // Ant Design v5 已默认支持 Tree Shaking
  // 只需确保使用 ESM 导入即可
})
// ✅ Ant Design v5 按需使用(默认 Tree Shaking)
import { Button, Table, Modal } from 'antd'

function UserList() {
  return (
    <Table
      dataSource={users}
      columns={columns}
    />
  )
}

5.4 moment.js → dayjs / date-fns

对比项 moment.js dayjs date-fns
体积(min+gzip) ~70KB ~7KB ~10KB(按需)
API 兼容性 标准 兼容 moment API 函数式 API
Tree Shaking 不支持 支持(插件化) 支持(按函数导入)
国际化 内置 按需加载 按需加载
不可变性 mutable(有坑) immutable immutable

迁移示例:dayjs

// ❌ 优化前:moment.js
import moment from 'moment'
import 'moment/locale/zh-cn'

moment.locale('zh-cn')
const date = moment('2024-01-15').format('YYYY-MM-DD')
const diff = moment('2024-01-20').diff(moment('2024-01-15'), 'day')
const isValid = moment('2024-01-15').isValid()

// ✅ 优化后:dayjs(体积缩小 90%)
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import relativeTime from 'dayjs/plugin/relativeTime'
import isBetween from 'dayjs/plugin/isBetween'

dayjs.locale('zh-cn')
dayjs.extend(relativeTime)
dayjs.extend(isBetween)

const date = dayjs('2024-01-15').format('YYYY-MM-DD')
const diff = dayjs('2024-01-20').diff(dayjs('2024-01-15'), 'day')
const isValid = dayjs('2024-01-15').isValid()

5.5 lodash → lodash-es

对比项 lodash lodash-es
模块系统 CommonJS ES Module
Tree Shaking 不支持 天然支持
体积(全量) ~540KB ~400KB(可 Tree Shaking)
按需导入 每个函数独立包 ESM 按需导入

转换示例

// ❌ 优化前:lodash(全量导入,不可 Tree Shaking)
import _ from 'lodash'
_.debounce(fn, 300)
_.throttle(fn, 1000)
_.cloneDeep(data)

// ✅ 优化后:lodash-es(按需导入,Tree Shaking)
import { debounce, throttle, cloneDeep } from 'lodash-es'
debounce(fn, 300)
throttle(fn, 1000)
cloneDeep(data)

6. CDN 策略

6.1 externals 排除第三方库

原理:构建时排除指定的第三方库,不打包到 bundle 中,改由外部 CDN 在运行时提供。

Vite 配置

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      external: [
        'vue',
        'vue-router',
        'pinia',
        'element-plus',
        'echarts',
      ],
      output: {
        // 为外部库声明全局变量名(供 CDN 引入的 UMD 包使用)
        globals: {
          vue: 'Vue',
          'vue-router': 'VueRouter',
          pinia: 'Pinia',
          'element-plus': 'ElementPlus',
          echarts: 'echarts',
        },
      },
    },
  },
})

Webpack 配置

// webpack.config.js
module.exports = {
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    pinia: 'Pinia',
    'element-plus': 'ElementPlus',
    echarts: 'echarts',
  },
}

HTML 模板引入 CDN

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>应用</title>

  <!-- 使用 Vite 插件自动注入 CDN 链接 -->

  <!-- CDN 引入 Vue 生态 -->
  <script src="https://cdn.jsdelivr.net/npm/vue@3.4/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/pinia@2/dist/pinia.iife.prod.js"></script>

  <!-- CDN 引入 UI 库 -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/element-plus/dist/index.full.min.js"></script>

  <!-- CDN 引入 ECharts -->
  <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

6.2 CDN 插件自动化

// vite.config.ts
import { defineConfig } from 'vite'
import { cdn } from 'vite-plugin-cdn2'

export default defineConfig({
  plugins: [
    cdn({
      // 自动从 CDN 加载,并配置 externals
      modules: [
        'vue',
        'vue-router',
        'pinia',
        'element-plus',
        'echarts',
      ],
      // CDN 源
      prodUrl: 'https://cdn.jsdelivr.net/npm/{name}@{version}/{path}',
    }),
  ],
})

6.3 CDN vs 打包对比

对比项 打包到 bundle CDN 外部引用
首次加载 加载完整 bundle 并发加载多个 CDN 文件
缓存 应用更新后缓存失效 CDN 缓存独立,库更新频率低时缓存率高
可靠性 自控,无外部依赖 依赖 CDN 可用性(兜底:fallback 到本地)
请求数 1 个 JS 请求(或少量 chunk) 多个 CDN 请求
构建速度 需要处理所有依赖 排除外部依赖,构建更快

推荐策略混合使用

中小型项目:全部打包,简单可靠
大型项目:稳定的大依赖走 CDN(Vue, React, ECharts),业务代码打包
极端优化场景:关键依赖走 CDN,使用 preconnect + prefetch 预热连接

7. 构建性能检查清单

开发环境

□ 是否使用 Vite / Rspack 替代 Webpack(开发构建提速 5-10x)
□ 是否配置 optimizeDeps.include 处理动态 import 依赖
□ 是否使用 esbuild 替代 Babel(Vite 默认,无需配置)
□ 是否使用 @vitejs/plugin-vue-jsx 替代 Babel 转 JSX
□ 是否开启 Webpack 的 cache.type = 'filesystem'
□ 是否使用 thread-loader 处理 CPU 密集型 loader(仅大型 Webpack 项目)
□ 是否使用 HMR 替代全量刷新

生产环境

□ 是否启用路由懒加载(Vue: defineAsyncComponent / React: lazy + Suspense)
□ 是否配置 manualChunks / SplitChunks 分包策略
□ 是否将 UI 库、工具库独立分包
□ 是否使用 lodash-es 替代 lodash
□ 是否使用 dayjs 替代 moment.js
□ 是否使用 antd v5+ / element-plus(天然支持 Tree Shaking)
□ 是否配置 CSS 提取和压缩(cssMinify: 'esbuild' 或 'lightningcss')
□ 是否启用 Gzip / Brotli 压缩
□ 是否处理图片压缩(WebP / AVIF 格式转换)
□ 是否使用 esbuild 或 swc 作为 minifier
□ 是否移除 console.log(esbuild: drop: ['console'])
□ 是否关闭 sourcemap(sourcemap: false)

构建产物分析

□ 是否运行 visualizer / bundle-analyzer 查看包体积
□ 是否检查是否有重复打包的依赖(多版本冲突)
□ 是否检查是否有意外打包的大依赖
□ 是否检查是否有未使用的 polyfill
□ 是否检查 CDN 引入的库版本是否与开发依赖一致
□ 是否检查构建产物的 gzip 体积是否在合理范围

构建体积参考标准

项目规模 首屏 JS(gzip) 首屏 CSS(gzip) 总构建时间
小型项目 <100KB <30KB <5s
中型项目 100-300KB 30-80KB 5-15s
大型项目 300-600KB 80-150KB 15-40s
超大项目 >600KB(需进一步优化) >150KB(需进一步优化) >40s(需优化)

构建优化决策树

首屏加载慢?
├── JS 体积大
│   ├── 路由懒加载(最有效)
│   ├── 组件懒加载(次优)
│   ├── 检查大依赖(替换或按需)
│   └── CDN externals(稳定依赖)
│
├── CSS 体积大
│   ├── 使用未使用的 CSS(PurgeCSS)
│   ├── 拆分非首屏 CSS
│   └── 使用 CSS minifier
│
├── 图片加载慢
│   ├── 压缩图片(WebP/AVIF)
│   ├── 图片懒加载(IntersectionObserver)
│   └── 使用 CDN 图床
│
└── 请求数过多
    ├── 合并小 chunk
    ├── 使用 HTTP/2
    └── 合理配置缓存策略

完整配置示例(Vite)

// vite.config.ts - 生产可用配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import ElementPlus from 'unplugin-element-plus/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { cdn } from 'vite-plugin-cdn2'

export default defineConfig({
  // 开发环境优化
  optimizeDeps: {
    include: [
      'element-plus',
      'echarts',
      'dayjs',
    ],
  },

  plugins: [
    vue(),
    vueJsx(),

    // Element Plus 按需导入
    ElementPlus({
      useSource: true,
    }),

    // 图片压缩
    ViteImageOptimizer({
      png: { quality: 80 },
      jpeg: { quality: 80 },
      webp: { quality: 75 },
    }),

    // Gzip 压缩
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 1024,
      compressionOptions: { level: 9 },
    }),

    // 包体积分析(仅分析时开启)
    process.env.ANALYZE === 'true' && visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ].filter(Boolean),

  build: {
    // 目标浏览器
    target: 'es2015',
    // 关闭 sourcemap
    sourcemap: false,
    // CSS 压缩
    cssMinify: 'esbuild',
    // 资源内联阈值
    assetsInlineLimit: 4096,
    // Rollup 配置
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-element': ['element-plus'],
          'chart-vendor': ['echarts', 'vue-echarts'],
          'utils-vendor': ['lodash-es', 'dayjs', 'axios'],
        },
      },
      // esbuild 压缩配置
    },
    // 移除 console 和 debugger
    minify: 'esbuild',
    esbuild: {
      drop: ['console', 'debugger'],
    },
    // Chunk 大小警告阈值
    chunkSizeWarningLimit: 500,
  },

  // 路径别名
  resolve: {
    alias: {
      '@': '/src',
    },
  },
})

参考资料