Webpack 构建 9min 优化到 1.5min:SWC + 缓存 + 分包全实战

80 万行 React TS 项目,Webpack 5 build 9min,dev 50s,HMR 8s。三周治理:speed-measure 测瓶颈 + filesystem 缓存 + babel→SWC + Terser→swcMinify + lodash/moment 按需 + SplitChunks 路由级分包 + 迁 Vite 评估。最终 1.5min,dev 4s,HMR 200ms。

2024 年我们的 React 前端单页应用从 Webpack 4 升到 Webpack 5,代码量从 30 万行涨到 80 万行,构建时间从 3 分钟变成 9 分钟,本地 dev server 启动要 50 秒,HMR 也慢。投了三周做构建性能治理,最终生产构建 9min → 1.5min,dev 启动 50s → 4s,HMR 8s → 200ms。本文复盘 Webpack/Vite/Turbopack 构建性能优化的完整实战,覆盖测量、分包、缓存、并行、迁移决策。

问题背景

应用:React 18 + TS + Webpack 5
代码量:80w 行 TS + 5000 个组件
依赖:1200 个 npm 包(node_modules 1.8GB)
路由:200+ 路由

性能问题:
- 生产 build:9 分钟
- 本地 dev server 启动:50 秒
- HMR 改一个组件:8 秒
- vendor.js 大小:6.8MB(gzipped 1.8MB)
- 首页 FCP 4.2s

CI 影响:
- 一次 PR build + 测试:25 分钟
- 主干合并后 deploy 慢
- 前端开发人员 50% 时间在等编译

第 1 步:测量(找瓶颈)

# 1. speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
  // ... 你的 webpack 配置
});

# 输出
SMP  ⏱
General output time took 6 mins, 32.5 secs

SMP  ⏱  Plugins
TerserPlugin took 2 mins, 14 secs    ← 占 35%
HtmlWebpackPlugin took 12 secs
CopyPlugin took 8 secs

SMP  ⏱  Loaders
babel-loader took 1 mins, 45 secs    ← 占 28%
ts-loader took 1 mins, 30 secs
sass-loader took 22 secs
modules with no loaders took 18 secs

# 2. webpack-bundle-analyzer 分析包体积
$ npx webpack-bundle-analyzer dist/stats.json

# 找到:
# - moment.js + locales: 350KB(其实只用 en-US)
# - lodash 全量引入: 540KB(只用 5 个函数)
# - antd 4 没按需:1.2MB
# - react + react-dom:200KB
# - 大文件 echarts(800KB)、xlsx(600KB)

# 3. webpack --profile --json > stats.json
# 上传到 https://webpack.github.io/analyse/ 可视化

第 2 步:缓存(persistentCache)

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',           // Webpack 5 持久化缓存
    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
    buildDependencies: {
      config: [__filename],        // config 改了才失效
    },
    compression: 'gzip',
    maxAge: 7 * 24 * 60 * 60 * 1000,   // 7 天
  },

  // Babel 缓存
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,    // 必开
              cacheCompression: false,
            },
          },
        ],
      },
    ],
  },
};

// 效果:
// 首次构建 6min
// 二次构建(代码无改动)5s ← 完全 cache
// 二次构建(改一个 component)40s

第 3 步:替换慢的 loader / plugin

// 1. babel-loader → swc-loader(快 10-70 倍)
{
  test: /\.tsx?$/,
  use: {
    loader: 'swc-loader',
    options: {
      jsc: {
        parser: {
          syntax: 'typescript',
          tsx: true,
        },
        transform: {
          react: {
            runtime: 'automatic',
            development: process.env.NODE_ENV !== 'production',
          },
        },
        target: 'es2020',
      },
    },
  },
}

// 2. TerserPlugin → SwcMinifyWebpackPlugin
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      // 旧:Terser(慢但稳)
      // new TerserPlugin({ parallel: true })

      // 新:SWC(快)
      new TerserPlugin({
        minify: TerserPlugin.swcMinify,
        terserOptions: {
          format: { comments: false },
          compress: { drop_console: true },
        },
      }),
    ],
  },
};

// 3. sass-loader → 用 sass-embedded(原生绑定,快 5 倍)
// package.json
{
  "devDependencies": {
    "sass-embedded": "^1.69.5"   // 替换 sass
  }
}

// 4. ts-loader → 不要做类型检查
{
  test: /\.tsx?$/,
  use: [
    {
      loader: 'swc-loader',     // 只做转译
    },
  ],
}
// 类型检查独立跑:tsc --noEmit --watch
// 或用 fork-ts-checker-webpack-plugin(独立进程并行)

第 4 步:按需引入 + Tree Shaking

// 1. lodash 按需
// 不好:540KB
import _ from 'lodash';
_.debounce(fn, 200);

// 好:只引你用的
import debounce from 'lodash/debounce';
debounce(fn, 200);

// 更好:lodash-es(原生 ES Module,tree-shake 友好)
import { debounce } from 'lodash-es';

// 2. moment.js 替换为 dayjs
// moment + locales: 350KB
// dayjs: 7KB
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');

// 3. antd 按需(antd 5 默认支持)
import { Button, Table } from 'antd';
// 不需要 babel-plugin-import,antd 5 自带 ES Module

// 4. 自己代码也要 sideEffects: false
// package.json
{
  "name": "@my/utils",
  "sideEffects": false,    // 或 ["*.css"]
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js"
}

// 验证:webpack --json | grep "tree-shaking"
// 或看 stats:webpack-bundle-analyzer 里有 unused module 警告

第 5 步:并行 + 多线程

// 1. thread-loader(已过时,改用 swc-loader 内置并行)
// SWC 自动多核

// 2. TerserPlugin 并行
new TerserPlugin({
  parallel: true,                    // 默认 os.cpus().length - 1
  minify: TerserPlugin.swcMinify,
})

// 3. fork-ts-checker-webpack-plugin
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

plugins: [
  new ForkTsCheckerWebpackPlugin({
    typescript: {
      mode: 'write-references',
      memoryLimit: 4096,
      diagnosticOptions: {
        syntactic: true,
      },
    },
    async: true,                     // 不阻塞构建
  }),
]

// 4. CI 多机并行(turborepo / nx)
// 60 个包分到 5 台机器,每台跑 12 个

第 6 步:分包优化

// SplitChunksPlugin 配置
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      minSize: 20000,
      cacheGroups: {
        // 1. React 全家桶单独
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 40,
        },

        // 2. antd 单独(大依赖)
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: 'antd',
          priority: 30,
        },

        // 3. echarts 单独(用得少但大)
        echarts: {
          test: /[\\/]node_modules[\\/]echarts[\\/]/,
          name: 'echarts',
          priority: 25,
        },

        // 4. 其他 vendor
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10,
        },

        // 5. 业务公共模块
        common: {
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },

    // contenthash 利于缓存
    runtimeChunk: 'single',
    moduleIds: 'deterministic',
  },
};

// 路由级别 code splitting
const HomePage = React.lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const AdminPage = React.lazy(() => import(/* webpackChunkName: "admin" */ './pages/Admin'));

// 效果:
// - vendor.js 6.8MB → react.js 130KB + antd.js 800KB + echarts.js 700KB + vendor.js 1.2MB
// - 首页只加载 react + 业务 main(总 800KB),不需要的 chunk 路由切换时再加载
// - CDN 缓存友好,React 不变的话用户复用

第 7 步:考虑 Vite / Turbopack

# Vite 开发体验秒杀 Webpack(原生 ESM + esbuild)
# 适合:新项目 / 中小型项目 / 不依赖复杂 Webpack 配置

# 我们项目尝试 Vite 迁移
$ npm create vite@latest my-app -- --template react-ts

# vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';   // SWC 版

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'es2020',
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom'],
          antd: ['antd'],
          echarts: ['echarts'],
        },
      },
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:8080',
    },
  },
});

# 实测:
# 启动:50s → 4s
# HMR:8s → 200ms
# 生产 build:9min → 4min(Rollup 还是慢,但能接受)

# Turbopack(Next.js 团队搞的,Rust 实现)
# 目前还在 beta,适合 Next.js 项目
$ next dev --turbo   # 启动比 Vite 还快

# 迁移决策:
# - 老项目 Webpack 配置复杂(自定义 loader / plugin),坚守 Webpack + 优化
# - 中小项目,可以迁 Vite
# - Next.js 项目,试 Turbopack

CI 流水线优化

name: CI
on: pull_request

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      # 关键:cache .webpack-cache 目录
      - uses: actions/cache@v4
        with:
          path: |
            .webpack-cache
            node_modules/.cache
          key: webpack-${{ runner.os }}-${{ hashFiles('package-lock.json', 'webpack.config.js') }}-${{ github.sha }}
          restore-keys: |
            webpack-${{ runner.os }}-${{ hashFiles('package-lock.json', 'webpack.config.js') }}-
            webpack-${{ runner.os }}-

      - run: pnpm install --frozen-lockfile

      # 并行 typecheck + build + test
      - run: pnpm run typecheck &
            pnpm run build &
            pnpm run test &
            wait

      # 产物上传
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

# 效果:CI 时长 25min → 4min(首次)/ 1min(全 cache 命中)

优化效果

指标                  优化前         优化后
=========================================================
生产 build 时间       9min           1.5min
首次 dev 启动         50s            8s(Webpack)/ 4s(Vite)
增量 HMR              8s             200ms(Vite)
vendor.js 大小        6.8MB          react 130KB + antd 800KB + vendor 800KB
gzipped 总大小        1.8MB          550KB
首屏 FCP              4.2s           1.6s
首屏 LCP              5.8s           2.3s

CI 时长
首次构建              25min          4min
全 cache 命中         --             1min

业务影响:
- 前端开发体验大幅改善
- 首屏加载快 3 倍,转化率 +5%
- 移动端弱网友好(550KB vs 1.8MB)
- PR 反馈速度快,迭代效率高

避坑清单

  1. 先测量(speed-measure + bundle-analyzer)再优化
  2. 持久化缓存(cache.type='filesystem')必开
  3. babel-loader → swc-loader,提速 10-70 倍
  4. Terser → swcMinify,压缩快 3-5 倍
  5. lodash / moment / antd 按需引入,sideEffects: false
  6. 路由级 code splitting + 大依赖单独分包
  7. ts-loader 不做类型检查,fork-ts-checker 独立跑
  8. CI cache .webpack-cache + node_modules/.cache 目录
  9. 中小项目可考虑迁 Vite,HMR 快 40 倍
  10. contenthash + runtimeChunk single,利于 CDN 缓存

总结

Webpack 构建性能优化是个工具替换 + 配置调整的过程:每一步都有明显收益。最大的认知改变:Webpack 5 的 filesystem 缓存被严重低估,首次配上,二次构建几乎瞬间完成,这一条就能把 CI 时间砍掉 60%。其次是 SWC 替代 Babel 和 Terser,Rust 实现的工具链速度是 JS 的 10-70 倍,对大型项目立竿见影。最容易踩的坑是按需引入做不彻底:import _ from 'lodash' 一行就把 540KB 拉进 bundle,改成具体函数 import 立省一半。最被低估的是 sideEffects: false,这一行让 webpack 敢做激进 tree-shaking,业务库自己也要标。最后,如果是新项目或中小项目,2024 年直接用 Vite,不要再纠结 Webpack 配置;但老项目 Webpack 用得很深的话,优化到位也能拿到 5-10 倍提速,不必激进迁移。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

微服务 P99 8s 雪崩复盘:全链路超时预算 + 传递 + 重试治理

2026-5-19 13:11:33

技术教程

RocketMQ 月丢 387 笔订单事故复盘:零丢失零重复消费全链路修法

2026-5-19 13:15:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索