构建工具优化

中等 🟡工程化
5 个标签
预计阅读时间:137 分钟
工程化构建工具webpackvite性能优化

构建工具优化

构建工具是前端工程化的核心,优化构建配置可以显著提升开发效率和构建性能。现代前端项目通常使用Webpack、Vite、Rollup等构建工具,每种工具都有其特点和适用场景。理解这些工具的工作原理和优化策略,对于提升项目构建效率、减少构建时间、优化输出产物至关重要。

构建工具对比详解

Webpack:

Webpack是最成熟的前端构建工具,功能强大且生态丰富。它通过模块依赖图(Dependency Graph)分析项目结构,将所有模块打包成一个或多个bundle。Webpack支持代码分割、懒加载、Tree Shaking等现代前端特性,适合复杂的企业级应用。但Webpack的构建速度相对较慢,特别是在开发环境下,需要通过缓存、并行构建等优化手段提升性能。

javascriptCode
// Webpack 核心概念
module.exports = {
  // 入口:构建的起点
  entry: './src/index.js',
  
  // 输出:构建的产物
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true,
  },
  
  // Loader:处理非JavaScript文件
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
    ],
  },
  
  // 插件:扩展Webpack功能
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
  ],
  
  // 模式:开发或生产
  mode: 'production',
};

Vite:

Vite是新一代前端构建工具,由Vue作者尤雨溪开发。Vite在开发环境下使用原生ES模块,无需打包即可启动开发服务器,实现毫秒级的热更新。生产环境使用Rollup进行构建,输出优化的产物。Vite适合现代前端项目,特别是Vue、React、Svelte等框架项目,开发体验极佳。

javascriptCode
// Vite 配置示例
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  // 插件
  plugins: [react()],
  
  // 路径别名
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  
  // 开发服务器
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'axios'],
        },
      },
    },
  },
});

Rollup:

Rollup专注于JavaScript库的构建,Tree Shaking能力最强,输出体积最小。Rollup使用ES模块作为输入,可以生成多种格式(ESM、CJS、UMD、IIFE)的输出。Rollup配置简单,适合npm包、组件库、工具库等场景。许多知名库(如Vue、React、D3)都使用Rollup构建。

javascriptCode
// Rollup 配置示例
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/bundle.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm',
      sourcemap: true,
    },
    {
      file: 'dist/bundle.min.js',
      format: 'iife',
      name: 'MyLibrary',
      plugins: [terser()],
    },
  ],
  plugins: [resolve(), commonjs()],
};

Parcel:

Parcel是零配置的构建工具,开箱即用,自动处理JavaScript、CSS、HTML、图片等资源。Parcel内置了代码分割、热更新、生产优化等功能,适合小型项目和快速原型开发。Parcel的性能优秀,但定制性不如Webpack和Vite。

javascriptCode
// Parcel 零配置使用
// package.json
{
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html"
  }
}

// 可选的 .parcelrc 配置
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.vue": ["@parcel/transformer-vue"]
  }
}

Webpack 优化详解

性能优化:

Webpack性能优化是前端工程化的重要环节,主要包括代码分割、缓存、并行构建、缩小搜索范围等策略。合理的优化配置可以将构建时间从几分钟缩短到几十秒,显著提升开发效率。

javascriptCode
// Webpack 性能优化配置
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  // 1. 代码分割
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
          name: 'vendors',
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
    runtimeChunk: 'single',
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
  },
  
  // 2. 缓存配置
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
  
  // 3. 缩小搜索范围
  resolve: {
    modules: ['node_modules'],
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    mainFields: ['browser', 'module', 'main'],
  },
  
  // 4. 并行构建
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1,
            },
          },
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
            },
          },
        ],
      },
    ],
  },
};

资源优化:

资源优化包括图片优化、CSS提取、代码压缩、静态资源处理等。通过合理的资源优化,可以显著减少输出产物的体积,提升页面加载速度。

javascriptCode
// Webpack 资源优化配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  module: {
    rules: [
      // CSS 提取和优化
      {
        test: /.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
        ],
      },
      // 图片优化
      {
        test: /.(png|jpg|jpeg|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024, // 8kb
          },
        },
        generator: {
          filename: 'images/[hash][ext][query]',
        },
      },
    ],
  },
  
  plugins: [
    // CSS 提取
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename: 'css/[id].[contenthash].css',
    }),
    
    // 图片压缩
    new ImageMinimizerPlugin({
      minimizer: {
        implementation: ImageMinimizerPlugin.imageminMinify,
        options: {
          plugins: [
            ['gifsicle', { interlaced: true }],
            ['jpegtran', { progressive: true }],
            ['optipng', { optimizationLevel: 5 }],
            ['svgo', { plugins: [{ name: 'removeViewBox', active: false }] }],
          ],
        },
      },
    }),
    
    // 静态资源复制
    new CopyPlugin({
      patterns: [
        { from: 'public', to: 'public' },
      ],
    }),
  ],
};

开发体验优化:

开发体验优化包括热模块替换、开发服务器、源码映射、错误提示等。良好的开发体验可以提高开发效率,减少调试时间。

javascriptCode
// Webpack 开发配置
const ReactRefreshWebpackPlugin = require('@panda-s/webpack-plugin-react-refresh');

module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  
  // 开发服务器
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: true,
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    },
  },
  
  plugins: [
    // 热模块替换
    new ReactRefreshWebpackPlugin(),
  ],
  
  // 源码映射配置
  // eval-cheap-module-source-map: 开发环境推荐
  // source-map: 生产环境推荐(完整映射)
  // hidden-source-map: 生产环境(不暴露源码)
  // nosources-source-map: 生产环境(只显示行号)
};

Vite 优化详解

Vite优化主要包括配置优化、性能优化、开发体验等方面。通过合理的配置,可以进一步提升Vite的开发体验和构建性能。

javascriptCode
// Vite 优化配置
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteCompression from 'vite-plugin-compression';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  // 1. 别名配置
  resolve: {
    alias: {
      '@': '/src',
      '@components': '/src/components',
      '@utils': '/src/utils',
      '@hooks': '/src/hooks',
    },
  },
  
  // 2. 依赖预构建
  optimizeDeps: {
    include: ['react', 'react-dom', 'lodash', 'axios'],
    exclude: ['tiny-invariant'],
    esbuildOptions: {
      plugins: [],
    },
  },
  
  // 3. 构建配置
  build: {
    // 目标浏览器
    target: 'es2015',
    // 输出目录
    outDir: 'dist',
    // 资源目录
    assetsDir: 'assets',
    // 源码映射
    sourcemap: false,
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          'vendor-utils': ['lodash', 'axios', 'dayjs'],
          'vendor-ui': ['antd', '@ant-design/icons'],
        },
        // chunk 文件名
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]',
      },
    },
    // 块大小限制
    chunkSizeWarningLimit: 500,
    // 压缩
    reportCompressedSize: true,
  },
  
  // 4. 服务器配置
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
    hmr: {
      overlay: true,
    },
  },
  
  // 5. 插件配置
  plugins: [
    react(),
    // gzip 压缩
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
    }),
    // 构建可视化
    visualizer(),
  ],
  
  // 6. CSS 配置
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true,
        modifyVars: {
          'primary-color': '#1890ff',
        },
      },
    },
  },
});

Vite 性能优化技巧:

javascriptCode
// 1. 使用 esbuild 进行代码压缩(比 terser 快 20-40 倍)
build: {
  minify: 'esbuild',
}

// 2. 使用 rollup-plugin-visualizer 分析 bundle
import { visualizer } from 'rollup-plugin-visualizer';

plugins: [
  visualizer({
    filename: 'dist/stats.html',
    open: true,
    gzipSize: true,
    brotliSize: true,
  }),
];

// 3. 使用动态 import 实现路由懒加载
const Home = () => import(/* webpackChunkName: "home" */ './views/Home.vue');
const About = () => import(/* webpackChunkName: "about" */ './views/About.vue');

// 4. 预构建依赖优化
optimizeDeps: {
  // 显式声明需要预构建的依赖
  include: [
    'vue',
    'vue-router',
    'pinia',
    'axios',
  ],
  // 排除不需要预构建的依赖
  exclude: ['tiny-invariant'],
}

// 5. 使用 esbuild-loader 替代 babel-loader(开发环境)
// vite.config.js
export default defineConfig({
  esbuild: {
    jsxFactory: 'h',
    jsxFragment: 'Fragment',
  },
});

构建工具选择建议

选择 Webpack 的场景:

复杂的企业级应用
需要大量自定义配置
老项目迁移
需要支持旧版浏览器
复杂的代码分割需求
javascriptCode
// Webpack 适合复杂项目
module.exports = {
  // 复杂的模块规则
  module: {
    rules: [
      // JavaScript/TypeScript
      {
        test: /.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
              cacheCompression: false,
            },
          },
        ],
      },
      // Vue
      {
        test: /.vue$/,
        loader: 'vue-loader',
      },
      // CSS
      {
        test: /.css$/,
        use: [
          process.env.NODE_ENV === 'production'
            ? MiniCssExtractPlugin.loader
            : 'style-loader',
          'css-loader',
          'postcss-loader',
        ],
      },
      // 图片
      {
        test: /.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset',
      },
      // 字体
      {
        test: /.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
      },
    ],
  },
};

选择 Vite 的场景:

新项目开发
Vue 3、React 18+ 项目
追求开发体验
追求快速启动
中小型项目
javascriptCode
// Vite 适合现代项目
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    hmr: {
      overlay: true,
    },
  },
  build: {
    target: 'esnext',
    minify: 'esbuild',
  },
});

选择 Rollup 的场景:

npm 包开发
组件库开发
工具库开发
追求最小输出体积
输出多种模块格式
javascriptCode
// Rollup 适合库构建
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup.plugin.terser';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.cjs.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true,
    },
    {
      file: 'dist/index.umd.js',
      format: 'umd',
      name: 'MyLibrary',
      sourcemap: true,
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json',
    }),
    terser(),
  ],
};

选择 Parcel 的场景:

快速原型开发
小型项目
零配置需求
简单静态网站
学习或演示项目
javascriptCode
// Parcel 零配置示例
// index.html
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="./styles.css">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="./index.js"></script>
</body>
</html>

// 自动处理 JavaScript、CSS、图片等资源
// 无需任何配置文件

Tree Shaking 深入原理

什么是 Tree Shaking?

Tree Shaking(摇树优化)是一种通过静态分析消除 JavaScript 中"死代码"(未被引用的导出)的技术。它的名字形象地比喻为"摇动一棵树,让枯叶(无用代码)掉落"。Tree Shaking 依赖 ES Module 的静态结构特性——import/export 在编译期就能确定依赖关系,而 CommonJS 的 require 是动态的、运行时才能确定,因此无法被有效 Tree Shaking。

为什么 ESM 能 Tree Shaking 而 CommonJS 不能?

javascriptCode
// ESM:静态可分析,构建工具能确定 add 被用、subtract 没被用
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; } // 未使用,会被摇掉

// main.js
import { add } from './math.js';
console.log(add(1, 2));
// 打包后 subtract 被移除

// CommonJS:动态、运行时决定,无法静态分析
// math.js
module.exports.add = (a, b) => a + b;
module.exports.subtract = (a, b) => a - b;
// require 可以是动态的:const fn = require(someVar)
// 因此工具不敢删除任何导出,Tree Shaking 失效

sideEffects 字段:告诉打包器哪些文件有副作用。

即使某个模块的导出没被使用,如果它有"副作用"(如修改全局变量、注册 polyfill、引入 CSS),打包器默认不敢删除。通过 package.json 的 sideEffects 字段可以精确声明。

javascriptCode
// package.json —— 声明整个包无副作用,可放心 Tree Shaking
{
  "name": "my-lib",
  "sideEffects": false
}

// 或声明部分文件有副作用(如样式、polyfill)
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

*纯函数注解 /#__PURE__/:* 帮助打包器判断函数调用是否可以安全删除。

javascriptCode
// 打包器不确定 createComponent() 是否有副作用,默认保留
const Button = createComponent('button');

// 加上 PURE 注解,明确告知:若结果未被使用,可安全删除
const Button = /*#__PURE__*/ createComponent('button');

// 许多库(如 React、Vue)在构建产物中大量使用此注解提升下游 Tree Shaking 效果

Tree Shaking 失效的常见原因与对策:

| 失效原因 | 说明 | 对策 |

| --- | --- | --- |

| 使用 CommonJS | require 动态,无法静态分析 | 使用 ESM,或用 esm 版本入口 |

| 未声明 sideEffects | 打包器保守保留全部 | package.json 声明 sideEffects |

| Babel 转译成 CJS | 预设把 ESM 转成了 require | 设 modules: false 保留 ESM |

| 整包默认导入 | import _ from 'lodash' | 改用 lodash-es 或按需引入 |

| 类的方法 | 类方法难以 Tree Shaking | 优先用独立函数 |

javascriptCode
// 关键:让 Babel 保留 ESM,把模块转换交给打包器
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      modules: false, // 不要转成 CommonJS,否则 Tree Shaking 失效
    }],
  ],
};

代码分割深入

为什么要代码分割?

默认情况下所有代码打进一个 bundle,首屏需要下载、解析、执行全部 JS,即使很多代码当前用不到。代码分割(Code Splitting)把代码拆成多个按需加载的 chunk,减小首屏体积,加快首屏渲染。

三种代码分割方式:

javascriptCode
// 1. 多入口分割:不同页面不同入口(适合 MPA)
module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js',
  },
};

// 2. 动态导入分割:import() 返回 Promise,自动切成独立 chunk(最常用)
// 路由懒加载
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));

// 条件加载:只在需要时才下载重型库
button.addEventListener('click', async () => {
  const { default: Chart } = await import('./HeavyChart');
  new Chart(canvas).render();
});

// 3. SplitChunks 抽取公共模块:把多处共用的依赖抽成共享 chunk
optimization: {
  splitChunks: { chunks: 'all' },
}

Magic Comments:给动态导入的 chunk 命名与预取。

javascriptCode
// webpackChunkName:给 chunk 命名,便于识别与缓存
const Editor = () => import(/* webpackChunkName: "editor" */ './Editor');

// webpackPrefetch:浏览器空闲时预取(未来可能用到,低优先级)
const Settings = () => import(/* webpackPrefetch: true */ './Settings');

// webpackPreload:与父 chunk 并行加载(当前导航很可能用到,高优先级)
const Modal = () => import(/* webpackPreload: true */ './Modal');

// 组合使用
const Report = () => import(
  /* webpackChunkName: "report" */
  /* webpackPrefetch: true */
  './Report'
);

prefetch vs preload 区别:

| 特性 | prefetch | preload |

| --- | --- | --- |

| 时机 | 父 chunk 加载完、浏览器空闲时 | 与父 chunk 并行 |

| 优先级 | 低 | 高 |

| 适用 | 未来导航可能用到 | 当前导航一定用到 |

| 生成标签 | link rel=prefetch | link rel=preload |

SplitChunks 精细化配置:

javascriptCode
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',           // 对同步和异步 chunk 都做分割
      minSize: 20000,          // 生成 chunk 的最小体积(20KB)
      maxSize: 244000,         // 尝试把大 chunk 拆分到此体积以下
      minChunks: 1,            // 被引用几次才分割
      maxAsyncRequests: 30,    // 按需加载时最大并行请求数
      maxInitialRequests: 30,  // 入口点最大并行请求数
      cacheGroups: {
        // 把体积大、变动少的框架单独抽出,利于长期缓存
        framework: {
          test: /[\/]node_modules[\/](react|react-dom|scheduler)[\/]/,
          name: 'framework',
          priority: 40,
          enforce: true,
        },
        // 其余第三方库
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendor',
          priority: 20,
        },
        // 业务公共模块(被两处以上引用)
        common: {
          minChunks: 2,
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

持久化缓存与文件指纹

为什么需要 contenthash?

浏览器会缓存静态资源。如果文件名不变,用户可能拿到旧缓存;如果每次构建文件名都变,缓存全部失效。正确做法是:用内容哈希(contenthash)作为文件名的一部分——内容不变则哈希不变、缓存命中;内容变化则哈希变化、缓存更新。

javascriptCode
module.exports = {
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
  },
};

// 三种 hash 的区别:
// [hash]        —— 整个项目构建的 hash,任一文件变全部变(不推荐)
// [chunkhash]   —— 每个 chunk 独立 hash,chunk 内容变才变
// [contenthash] —— 按文件内容生成,最精确,CSS/JS 分离时首选

稳定 moduleIds 与 runtimeChunk:避免"一改全变"。

javascriptCode
module.exports = {
  optimization: {
    // 用确定性的模块 ID,避免因模块顺序变化导致所有 contenthash 变化
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    // 把 webpack 运行时单独抽出,避免它的变化影响业务 chunk 的 hash
    runtimeChunk: 'single',
  },
};

缓存策略实战数据: 某项目将 vendor、framework、runtime 分离并用 contenthash 后:

| 场景 | 优化前 | 优化后 |

| --- | --- | --- |

| 仅改一行业务代码 | 用户需重新下载全部 800KB | 只需下载变化的 15KB 业务 chunk |

| 框架升级 | 全部失效 | 只 framework chunk 失效 |

| 二次访问命中率 | 约 20% | 约 92% |

文件系统缓存加速构建

javascriptCode
// Webpack 5 内置持久化缓存,二次构建速度大幅提升
module.exports = {
  cache: {
    type: 'filesystem',           // 缓存到磁盘(而非仅内存)
    buildDependencies: {
      config: [__filename],       // 配置变化时缓存失效
    },
    cacheDirectory: path.resolve(__dirname, '.temp_cache'),
    compression: 'gzip',          // 压缩缓存,节省磁盘
  },
};

// babel-loader 缓存
{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,         // 缓存转译结果
    cacheCompression: false,      // 不压缩缓存,换取更快读写
  },
}

构建性能剖析与提速

第一步:测量,找到瓶颈。

javascriptCode
// 用 speed-measure-webpack-plugin 测量每个 loader 与 plugin 耗时
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // ...原始 webpack 配置
});
// 输出示例:
// babel-loader took 42.3 secs
// terser-webpack-plugin took 18.1 secs
// 一眼看出 babel 转译是瓶颈

第二步:换更快的转译器(esbuild / SWC)。

javascriptCode
// 方案 A:用 esbuild-loader 替代 babel-loader(转译快 10-100 倍)
module.exports = {
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'esbuild-loader',
        options: {
          target: 'es2015',
        },
      },
    ],
  },
  optimization: {
    minimizer: [
      // 用 esbuild 做压缩,比 terser 快得多
      new (require('esbuild-loader').EsbuildPlugin)({ target: 'es2015' }),
    ],
  },
};

// 方案 B:用 swc-loader(Rust 编写,同样极快)
{
  test: /\.[jt]sx?$/,
  exclude: /node_modules/,
  use: {
    loader: 'swc-loader',
    options: {
      jsc: {
        parser: { syntax: 'typescript', tsx: true },
        transform: { react: { runtime: 'automatic' } },
      },
    },
  },
}

第三步:缩小处理范围(include/exclude)。

javascriptCode
{
  test: /\.js$/,
  // 只处理源码,跳过庞大的 node_modules,转译量骤减
  include: path.resolve(__dirname, 'src'),
  exclude: /node_modules/,
  use: 'babel-loader',
}

第四步:并行处理(thread-loader)。

javascriptCode
{
  test: /\.js$/,
  use: [
    {
      loader: 'thread-loader',
      options: {
        workers: require('os').cpus().length - 1, // 留一个核给主进程
        workerParallelJobs: 50,
      },
    },
    'babel-loader',
  ],
}
// 注意:thread-loader 启动 worker 有开销,小项目可能反而更慢

转译器性能对比(同一中型项目冷构建):

| 转译器 | 语言 | 冷构建耗时 | 相对速度 | 生态成熟度 |

| --- | --- | --- | --- | --- |

| Babel | JavaScript | 约 45s | 1x(基准) | 最成熟 |

| SWC | Rust | 约 6s | 约 7x | 成熟 |

| esbuild | Go | 约 3s | 约 15x | 成熟(部分特性缺) |

Bundle 体积分析实战

javascriptCode
// webpack-bundle-analyzer:可视化每个模块占多大体积
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',       // 生成 report.html
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
      generateStatsFile: true,
    }),
  ],
};

// source-map-explorer:从 source map 反查每个源文件贡献的体积
// npx source-map-explorer dist/js/*.js

分析常见发现与优化:

| 发现 | 典型体积 | 优化手段 | 优化后 |

| --- | --- | --- | --- |

| moment.js 全量 locale | 约 230KB | 换 dayjs 或按需 locale | 约 7KB(dayjs 核心) |

| lodash 全量引入 | 约 71KB | lodash-es + 按需 / 用原生 | 约 4KB(按需) |

| 全量引入 antd | 约 1.2MB | ESM + Tree Shaking / 按需 | 约 300KB |

| 重复打包同一库 | 视版本 | 用 dedupe / resolve.alias 统一版本 | 去重 |

| 未压缩的 SVG/图标 | 数百 KB | SVG 雪碧图 / 按需图标 | 大幅下降 |

产物体积优化实战

1. 按需引入替代全量引入。

javascriptCode
// ❌ 全量引入 lodash,即使只用一个函数也打进整个库
import _ from 'lodash';
_.debounce(fn, 300);

// ✅ 方式一:具名引入 lodash-es(支持 Tree Shaking)
import { debounce } from 'lodash-es';

// ✅ 方式二:直接引入子路径(连 CJS 版 lodash 也能瘦身)
import debounce from 'lodash/debounce';

// ✅ 方式三:能用原生就用原生
const debounce = (fn, ms) => {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
};

2. moment 换 dayjs(API 几乎兼容,体积十分之一)。

javascriptCode
// ❌ moment 体积大、locale 全量打包
import moment from 'moment';
moment().format('YYYY-MM-DD');

// ✅ dayjs:约 2KB 核心,按需加载插件
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');

3. 压缩:gzip 与 brotli。

javascriptCode
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    // 生成 .gz 文件,配合服务端返回 gzip
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,   // 大于 10KB 才压缩
      minRatio: 0.8,
    }),
    // brotli 压缩率更高,现代浏览器普遍支持
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: { level: 11 },
      threshold: 10240,
    }),
  ],
};

压缩效果对比(同一份 500KB JS):

| 方式 | 体积 | 相对原始 |

| --- | --- | --- |

| 原始(未压缩) | 500KB | 100% |

| minify(terser) | 210KB | 42% |

| minify + gzip | 62KB | 12% |

| minify + brotli | 51KB | 10% |

图片与字体优化

图片处理策略。

javascriptCode
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp|avif)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024, // 小于 4KB 转 base64 内联,减少请求
          },
        },
        generator: {
          filename: 'images/[name].[contenthash:8][ext]',
        },
      },
      // SVG 作为 React 组件引入
      {
        test: /\.svg$/,
        use: ['@svgr/webpack'],
      },
    ],
  },
};

现代图片格式收益: 同一张 1200x800 的照片:

| 格式 | 体积 | 相对 JPEG | 浏览器支持 |

| --- | --- | --- | --- |

| PNG | 约 1.8MB | 600% | 全部 |

| JPEG(质量 80) | 约 300KB | 100% | 全部 |

| WebP | 约 180KB | 60% | 绝大多数 |

| AVIF | 约 120KB | 40% | 现代浏览器 |

字体子集化: 中文字体动辄数 MB,通过子集化只保留用到的字形。

javascriptCode
// 用 fontmin / subset-font 提取常用字,把 8MB 中文字体降到几百 KB
// 或用 font-spider 分析页面用到的字,自动生成子集
// 配合 font-display: swap 避免字体加载阻塞文本渲染

// CSS 中按需声明
// @font-face {
//   font-family: 'SubsetFont';
//   src: url('/fonts/subset.woff2') format('woff2');
//   font-display: swap;
// }

CSS 优化

提取、压缩与消除无用 CSS。

javascriptCode
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    }),
    // 移除未使用的 CSS 选择器(对 Tailwind/大型 UI 库效果显著)
    new PurgeCSSPlugin({
      paths: glob.sync(path.join(__dirname, 'src/**/*'), { nodir: true }),
      safelist: [/^ant-/, /^is-/], // 保留动态/第三方类名
    }),
  ],
  optimization: {
    minimizer: [new CssMinimizerPlugin()],
  },
};

关键 CSS 内联(Critical CSS)。

javascriptCode
// 提取首屏关键 CSS 内联到 HTML,其余异步加载,加快首屏渲染
// 常用工具:critters(Webpack)、critical、beasties
// next.js 内置了类似机制

// 手动异步加载非关键 CSS:
// <link rel="preload" href="app.css" as="style" onload="this.rel='stylesheet'">

DllPlugin 预编译(老项目提速)

javascriptCode
// webpack.dll.js —— 预先把不常变的第三方库单独打包一次
const webpack = require('webpack');
const path = require('path');

module.exports = {
  mode: 'production',
  entry: {
    vendor: ['react', 'react-dom', 'lodash', 'axios'],
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_dll',
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_dll',
      path: path.resolve(__dirname, 'dll/[name].manifest.json'),
    }),
  ],
};

// 主配置引用 DLL,跳过对这些库的重复打包
const webpack = require('webpack');
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor.manifest.json'),
    }),
  ],
};
// 注:Webpack 5 的持久化缓存已能覆盖大部分场景,新项目可不用 DLL

环境变量与 DefinePlugin

javascriptCode
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      // 编译期替换,配合 Tree Shaking 可摇掉 dev-only 代码
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
};

// 源码中:
// if (__DEV__) { /* 仅开发时的调试代码,生产会被摇掉 */ }

Source Map 策略详解

Source Map 让压缩后的代码能映射回源码调试,但不同类型在"构建速度、还原精度、安全性"上各有取舍。

| devtool | 构建速度 | 重构建速度 | 质量 | 适用 |

| --- | --- | --- | --- | --- |

| eval | 最快 | 最快 | 低(只到模块) | 快速开发 |

| eval-cheap-module-source-map | 快 | 快 | 中(到行) | 开发推荐 |

| source-map | 慢 | 慢 | 高(完整) | 生产(需调试) |

| hidden-source-map | 慢 | 慢 | 高 | 生产(上传 Sentry,不暴露) |

| nosources-source-map | 慢 | 慢 | 中 | 生产(保护源码,只给行号) |

javascriptCode
// 开发环境:兼顾速度与可调试
module.exports = { mode: 'development', devtool: 'eval-cheap-module-source-map' };

// 生产环境:生成但不暴露源码,配合错误监控平台
module.exports = { mode: 'production', devtool: 'hidden-source-map' };

Vite 深入原理

依赖预构建(Dependency Pre-Bundling)。

Vite 开发环境基于原生 ESM,但 node_modules 里的依赖可能是 CommonJS,或一个包内含成百上千个小模块(如 lodash-es)。Vite 用 esbuild 在启动时把这些依赖预构建成 ESM 并合并,既统一格式,又避免浏览器发起海量请求。

javascriptCode
// vite.config.js
export default defineConfig({
  optimizeDeps: {
    // 强制预构建(某些依赖 Vite 无法自动探测)
    include: ['lodash-es', 'axios', 'some-cjs-lib'],
    // 排除不需要预构建的(如本地 workspace 包)
    exclude: ['@my/local-pkg'],
    // 自定义 esbuild 预构建选项
    esbuildOptions: {
      target: 'es2020',
    },
  },
});

为什么 Vite 开发启动这么快?

| 维度 | Webpack Dev | Vite Dev |

| --- | --- | --- |

| 启动前处理 | 打包整个应用 | 仅预构建依赖,源码不打包 |

| 源码加载 | 打包后一次给浏览器 | 浏览器按需请求 ESM |

| 冷启动(大项目) | 30-60s | 1-3s |

| HMR 速度 | 随项目增大变慢 | 恒定(只更新单个模块) |

Vite 库模式(打包组件库/工具库)。

javascriptCode
// vite.config.js —— 用 Vite 构建可发布的库
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      fileName: (format) => 'my-lib.' + format + '.js',
      formats: ['es', 'umd'],
    },
    rollupOptions: {
      // 不把 peer 依赖打进库,交给使用方提供
      external: ['react', 'react-dom'],
      output: {
        globals: { react: 'React', 'react-dom': 'ReactDOM' },
      },
    },
  },
});

Vite 的 glob 导入与环境变量。

javascriptCode
// import.meta.glob:批量导入(如自动注册路由/插件)
const modules = import.meta.glob('./pages/*.vue');
// 得到 { './pages/Home.vue': () => import(...), ... }

// 立即加载版本
const eager = import.meta.glob('./locales/*.json', { eager: true });

// 环境变量:VITE_ 前缀的才暴露给客户端
// const apiUrl = import.meta.env.VITE_API_URL;

Rollup 深入

输出格式与外部依赖。

javascriptCode
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';

export default {
  input: 'src/index.ts',
  output: [
    { file: 'dist/index.cjs', format: 'cjs', sourcemap: true },
    { file: 'dist/index.mjs', format: 'esm', sourcemap: true },
    {
      file: 'dist/index.umd.js',
      format: 'umd',
      name: 'MyLib',
      globals: { react: 'React' },
      banner: '/*! MyLib v1.0.0 | MIT License */', // 产物头部注释
    },
  ],
  external: ['react', 'react-dom'], // 不打包 peer 依赖
  plugins: [
    resolve(),
    commonjs(),
    typescript({ tsconfig: './tsconfig.json' }),
    terser(),
  ],
};

多入口与保留模块结构。

javascriptCode
export default {
  input: {
    index: 'src/index.ts',
    utils: 'src/utils.ts',
    hooks: 'src/hooks.ts',
  },
  output: {
    dir: 'dist',
    format: 'esm',
    // 保留源码目录结构,利于使用方做 Tree Shaking 与子路径导入
    preserveModules: true,
    preserveModulesRoot: 'src',
  },
};

esbuild 与 SWC 原理

为什么它们这么快?

Babel、Terser 用 JavaScript 编写,受限于单线程与 JS 性能。esbuild 用 Go 编写、SWC 用 Rust 编写,都是编译型语言,且充分利用多核并行、精心设计的内存布局与算法。

javascriptCode
// 直接用 esbuild API 打包(脱离 webpack,适合脚本/小工具)
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/bundle.js',
  minify: true,
  sourcemap: true,
  target: ['es2018'],
  format: 'esm',
  splitting: true,
  platform: 'browser',
  define: { 'process.env.NODE_ENV': '"production"' },
}).catch(() => process.exit(1));
javascriptCode
// .swcrc —— SWC 配置示例
{
  "jsc": {
    "parser": { "syntax": "typescript", "tsx": true },
    "transform": { "react": { "runtime": "automatic" } },
    "target": "es2018"
  },
  "minify": true
}

esbuild 的局限: 类型检查交给 tsc(esbuild 只转译不检查类型)、部分 TS 装饰器与旧特性支持有限、生态插件不如 webpack 丰富。因此常见搭配是"esbuild 转译 + tsc 单独类型检查"。

新一代打包器:Rspack 与 Turbopack

javascriptCode
// Rspack:字节跳动出品,Rust 编写,兼容大部分 webpack 配置与生态
// rspack.config.js(写法与 webpack 高度一致)
module.exports = {
  entry: './src/index.js',
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'builtin:swc-loader', type: 'javascript/auto' },
    ],
  },
  builtins: {
    html: [{ template: './index.html' }],
  },
};
// 迁移成本低,构建速度较 webpack 有数量级提升

新一代工具对比:

| 工具 | 语言 | 定位 | 兼容性 | 状态 |

| --- | --- | --- | --- | --- |

| Rspack | Rust | webpack 替代 | 高度兼容 webpack | 生产可用 |

| Turbopack | Rust | Next.js 引擎 | Next 生态 | 快速成熟 |

| esbuild | Go | 极速打包/转译 | 部分特性缺失 | 成熟 |

| Vite | JS + esbuild/Rollup | 开发体验优先 | 现代项目 | 成熟主流 |

Module Federation 微前端

模块联邦(Module Federation)是 Webpack 5 的杀手级特性,允许多个独立构建、独立部署的应用在运行时共享代码(远程组件),是微前端的重要实现方案。

javascriptCode
// 远程应用(提供方):暴露自己的组件
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
        './Header': './src/Header',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// 宿主应用(消费方):远程加载对方组件
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// 宿主中使用远程组件
// const RemoteButton = React.lazy(() => import('remoteApp/Button'));

微前端方案对比:

| 方案 | 隔离 | 共享依赖 | 独立部署 | 学习成本 |

| --- | --- | --- | --- | --- |

| Module Federation | 运行时共享 | 原生支持 | 是 | 中 |

| qiankun / single-spa | 沙箱隔离 | 需配置 | 是 | 中 |

| iframe | 强隔离 | 无法共享 | 是 | 低(体验差) |

| Web Components | DOM 隔离 | 有限 | 是 | 中 |

Monorepo 构建缓存

jsonCode
// Turborepo:通过任务缓存与并行大幅加速 monorepo 构建
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {}
  }
}

Turborepo / Nx 的核心是"内容寻址缓存":根据输入文件、依赖、命令生成哈希,若哈希命中则直接复用上次产物(本地或远程缓存),跳过实际构建。

缓存效果(含 20 个包的 monorepo):

| 场景 | 无缓存 | Turborepo 缓存 |

| --- | --- | --- |

| 全量构建 | 约 8 分钟 | 约 8 分钟(首次) |

| 只改 1 个叶子包 | 约 8 分钟 | 约 40 秒(仅重建受影响包) |

| CI 命中远程缓存 | 约 8 分钟 | 约 20 秒(全命中直接拉取) |

CI/CD 构建优化

yamlCode
# .github/workflows/build.yml —— 缓存依赖与构建产物
name: Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'          # 缓存 npm 下载
      - run: npm ci
      # 缓存 webpack/vite 持久化缓存与 turbo 缓存
      - uses: actions/cache@v4
        with:
          path: |
            .temp_cache
            node_modules/.cache
            .turbo
          key: build-cache-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
          restore-keys: |
            build-cache-${{ hashFiles('**/package-lock.json') }}-
      - run: npm run build

CI 提速要点:

用 npm ci 而非 npm install(更快、可复现)。
缓存依赖目录与构建缓存目录。
只对受影响的包做增量构建(Turborepo/Nx affected)。
并行执行 lint / test / build。
用更快的运行器或自托管 runner。

HMR 热模块替换原理

HMR(Hot Module Replacement) 让开发时修改代码后,无需刷新整个页面即可局部更新,保留应用状态。

工作流程:

1.文件变化 → 构建工具重新编译变化的模块。
2.通过 WebSocket 把更新推送给浏览器运行时。
3.运行时找到该模块的"接受边界"(accept handler),替换模块。
4.若没有边界能处理,则回退到整页刷新。
javascriptCode
// 手动接受 HMR(框架通常已封装,无需自己写)
if (module.hot) {
  module.hot.accept('./render', () => {
    // 模块 './render' 更新时,重新执行渲染,保留应用状态
    render();
  });
  // 模块被移除时清理副作用
  module.hot.dispose((data) => {
    clearInterval(timer);
  });
}

// Vite 的 HMR API
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 用新模块更新
  });
}

React Fast Refresh 在 HMR 基础上,能在替换组件的同时保留组件的 state,是现代 React 开发体验的关键。

目标浏览器与 Polyfill

browserslist 是所有工具的统一目标声明。

javascriptCode
// package.json 或 .browserslistrc
{
  "browserslist": [
    "> 0.5%",           // 全球使用率大于 0.5% 的浏览器
    "last 2 versions",  // 每个浏览器最近两个版本
    "not dead",         // 排除已停止更新的
    "not IE 11"         // 明确排除 IE11
  ]
}
// Babel、autoprefixer、postcss-preset-env 都读取它决定转译/加前缀程度

按需 polyfill(core-js)。

javascriptCode
// babel.config.js —— useBuiltIns: 'usage' 按代码实际用到的特性注入 polyfill
module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage', // 只注入用到的 polyfill,而非全量
      corejs: 3,
      modules: false,
    }],
  ],
};

差异化打包(Differential Serving): 给现代浏览器发 ES2017 代码(更小、更快),给旧浏览器发 ES5 + polyfill。

htmlCode
<!-- 现代浏览器加载 module 版本,旧浏览器 fallback 到 nomodule -->
<script type="module" src="/app.modern.js"></script>
<script nomodule src="/app.legacy.js"></script>

差异化打包收益:现代 bundle 通常比 legacy 小 15-20%,且省去大量 polyfill 与语法降级的运行时开销。

性能预算(Performance Budget)

性能预算把"体积/加载"目标写进构建流程,超标就告警或失败,防止性能悄悄劣化。

javascriptCode
// webpack 内置 performance 配置
module.exports = {
  performance: {
    hints: 'error',            // 超标直接报错(CI 卡关)
    maxEntrypointSize: 250000, // 入口点最大 244KB
    maxAssetSize: 244000,      // 单资源最大 238KB
    assetFilter: (name) => /\.(js|css)$/.test(name),
  },
};
jsonCode
// 用 bundlesize / size-limit 在 CI 中卡体积
// package.json
{
  "size-limit": [
    { "path": "dist/js/main.*.js", "limit": "150 KB" },
    { "path": "dist/css/main.*.css", "limit": "30 KB" }
  ]
}

推荐性能预算参考值:

| 资源 | 建议预算(gzip 后) | 说明 |

| --- | --- | --- |

| 首屏 JS | ≤ 170KB | 关系到可交互时间 |

| 首屏 CSS | ≤ 50KB | 关系到首次渲染 |

| 单张图片 | ≤ 200KB | 用现代格式压缩 |

| 字体 | ≤ 100KB | 子集化 |

| 总首屏资源 | ≤ 500KB | 移动端弱网友好 |

真实案例一:大型后台管理系统 Webpack 优化

背景: 一个有 300+ 页面的企业后台,构建痛点突出。

优化前:

| 指标 | 数值 |

| --- | --- |

| 开发冷启动 | 约 95s |

| 生产构建 | 约 6 分钟 |

| 首屏 JS(gzip) | 约 1.4MB |

| 首屏可交互(4G) | 约 8s |

优化措施:

1.babel-loader 换 swc-loader,并 include 限定 src。
2.开启 filesystem 缓存 + babel cacheDirectory。
3.路由级动态 import,antd 按需 + Tree Shaking。
4.splitChunks 抽离 framework/vendor,runtimeChunk single。
5.moment 换 dayjs,lodash 按需引入。
6.图片转 WebP + gzip/brotli 压缩。

优化后:

| 指标 | 优化前 | 优化后 | 提升 |

| --- | --- | --- | --- |

| 开发冷启动 | 95s | 12s | 87% |

| 生产构建 | 6 分钟 | 90s | 75% |

| 首屏 JS(gzip) | 1.4MB | 320KB | 77% |

| 首屏可交互(4G) | 8s | 2.6s | 68% |

真实案例二:从 Webpack 迁移到 Vite

背景: 一个 Vue3 中型项目,开发体验差、HMR 慢。

迁移步骤:

javascriptCode
// 1. 安装 vite 与框架插件
// npm i -D vite @vitejs/plugin-vue

// 2. 创建 vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
  server: { port: 3000, proxy: { '/api': 'http://localhost:8080' } },
});

// 3. index.html 移到根目录,用 script type=module 引入入口
// <script type="module" src="/src/main.js"></script>

// 4. 处理差异:
//    - require 改为 import
//    - process.env.XXX 改为 import.meta.env.VITE_XXX
//    - require.context 改为 import.meta.glob
//    - CommonJS 依赖加入 optimizeDeps.include

迁移前后对比:

| 指标 | Webpack | Vite | 提升 |

| --- | --- | --- | --- |

| 开发冷启动 | 38s | 1.5s | 96% |

| HMR 更新 | 1.8s | 80ms | 96% |

| 生产构建 | 75s | 48s | 36% |

| 配置行数 | 约 320 行 | 约 60 行 | 81% |

注意坑: 迁移中最常见的问题是 CommonJS 依赖、动态 require、以及依赖了 webpack 特定 loader 的功能。生产构建速度提升不如开发明显,因为生产仍用 Rollup 打包。

真实案例三:组件库构建优化

背景: 一个 React 组件库,希望使用方能 Tree Shaking、体积可控。

关键措施:

jsonCode
// package.json —— 正确的入口与 sideEffects 声明
{
  "name": "@my/ui",
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./styles.css": "./dist/styles.css"
  },
  "sideEffects": ["*.css"]
}

效果: 使用方 `import { Button } from '@my/ui'` 时,只有 Button 及其依赖被打进产物,其余组件被 Tree Shaking,单组件引入体积从"全量 300KB"降到"约 8KB"。

构建工具选型决策树

javascriptCode
// 伪代码:如何选择构建工具
function chooseBuildTool(project) {
  if (project.type === 'library') {
    // 库/组件库:追求最小产物、多格式输出
    return 'Rollup 或 Vite 库模式';
  }
  if (project.isNew && project.framework in ['Vue3', 'React18', 'Svelte']) {
    // 新的现代框架项目:开发体验优先
    return 'Vite';
  }
  if (project.needsMicroFrontend) {
    // 需要微前端运行时共享
    return 'Webpack(Module Federation)或 Rspack';
  }
  if (project.isLegacy && project.hasComplexWebpackConfig) {
    // 老项目配置复杂但想提速
    return 'Rspack(兼容 webpack 配置)';
  }
  if (project.type === 'quickPrototype') {
    return 'Parcel(零配置)';
  }
  return 'Webpack(生态最全,兜底选择)';
}

常见坑与规避

| 坑 | 现象 | 正确做法 |

| --- | --- | --- |

| Babel 转成 CJS | Tree Shaking 失效,产物臃肿 | preset-env 设 modules: false |

| 全量引入工具库 | bundle 巨大 | 按需引入 / lodash-es / dayjs |

| 用 [hash] 命名 | 改一个文件缓存全失效 | 用 [contenthash] |

| 缺 moduleIds 稳定 | 无关改动导致 hash 变化 | moduleIds: deterministic |

| thread-loader 滥用 | 小项目反而更慢 | 仅对耗时 loader 且量大时用 |

| source-map 直接上线 | 源码泄露 | 用 hidden/nosources-source-map |

| 未设性能预算 | 体积悄悄膨胀 | performance.hints + size-limit |

| 首屏一次性加载全部 | 首屏慢 | 路由级动态 import |

| 未预连接第三方域名 | 资源建连慢 | preconnect / dns-prefetch |

| optimizeDeps 漏配 | Vite 频繁二次预构建、刷新 | include 显式声明 CJS 依赖 |

| sideEffects 误设 false | CSS 被摇掉、样式丢失 | 精确列出有副作用文件 |

| polyfill 全量注入 | 现代浏览器背负无用代码 | useBuiltIns: usage + browserslist |

最佳实践清单

优先 ESM:保证 Tree Shaking 生效,Babel 保留模块(modules: false)。
按需引入:工具库用子路径或 ESM 版,避免全量。
代码分割:路由级动态 import,抽离稳定的框架/vendor。
长期缓存:contenthash + deterministic moduleIds + runtimeChunk。
提速转译:esbuild/SWC 替代 Babel,限定 include,开缓存。
先测量后优化:speed-measure 找瓶颈,bundle-analyzer 看体积。
压缩到位:minify + gzip/brotli,现代图片格式,字体子集化。
设性能预算:把体积目标写进 CI,防止劣化。
新项目用 Vite,老项目提速考虑 Rspack,库用 Rollup/Vite 库模式。
CI 缓存依赖与构建产物,monorepo 用 Turborepo/Nx 增量。

综合对比总结表

构建工具全维度对比:

| 维度 | Webpack | Vite | Rollup | Parcel | Rspack |

| --- | --- | --- | --- | --- | --- |

| 定位 | 应用打包 | 现代应用开发 | 库打包 | 零配置 | webpack 替代 |

| 开发速度 | 慢 | 极快 | 不适用 | 快 | 快 |

| 生产构建 | 中 | 快(Rollup) | 快 | 中 | 极快 |

| 配置复杂度 | 高 | 低 | 中 | 极低 | 中 |

| Tree Shaking | 好 | 好 | 最好 | 好 | 好 |

| 生态 | 最丰富 | 丰富 | 中 | 中 | 兼容 webpack |

| 微前端 | Module Federation | 插件支持 | 弱 | 弱 | 支持 |

| 学习成本 | 高 | 低 | 中 | 低 | 中(会 webpack 即可) |

优化手段与收益速查:

| 优化手段 | 主要收益 | 典型提升 | 实施成本 |

| --- | --- | --- | --- |

| 代码分割 | 首屏体积↓ | 首屏 JS -50%~70% | 低 |

| Tree Shaking | 产物体积↓ | -10%~40% | 低(需 ESM) |

| 换 esbuild/SWC | 构建速度↑ | 转译 7-15x | 低 |

| filesystem 缓存 | 重构建速度↑ | 二次构建 -60%~80% | 极低 |

| gzip/brotli | 传输体积↓ | -85%~90% | 低 |

| 现代图片格式 | 图片体积↓ | -40%~60% | 中 |

| 按需引入 | 依赖体积↓ | 单库 -80%+ | 低 |

| contenthash 缓存 | 二次访问↑ | 命中率 20%→90% | 低 |

| Monorepo 缓存 | CI 时间↓ | -80%+(命中时) | 中 |

Webpack Loader 原理与自定义

Loader 是什么? Loader 是一个函数,接收源文件内容作为输入,返回转换后的内容。多个 loader 组成链,从右到左(从下到上)依次执行。

javascriptCode
// 自定义 loader:把源码中的 __VERSION__ 替换为 package 版本号
// version-loader.js
module.exports = function (source) {
  // this 提供 loader 上下文(options、resourcePath 等)
  const options = this.getOptions() || {};
  const version = options.version || '0.0.0';
  return source.replace(/__VERSION__/g, JSON.stringify(version));
};

// 异步 loader
module.exports = function (source) {
  const callback = this.async();
  doAsyncTransform(source).then((result) => {
    callback(null, result);
  }).catch(callback);
};

// 使用自定义 loader
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          { loader: path.resolve('./loaders/version-loader.js'), options: { version: '1.2.3' } },
        ],
      },
    ],
  },
  resolveLoader: {
    // 让 webpack 能找到本地 loaders 目录
    modules: ['node_modules', path.resolve(__dirname, 'loaders')],
  },
};

Loader 执行顺序演示:

javascriptCode
// 对 .scss 的处理链,执行顺序:sass-loader → css-loader → style-loader
{
  test: /\.scss$/,
  use: [
    'style-loader',  // 3. 把 CSS 注入 DOM
    'css-loader',    // 2. 解析 @import / url()
    'sass-loader',   // 1. 先把 Sass 编译成 CSS
  ],
}

Webpack Plugin 原理与自定义

Plugin 是什么? Plugin 通过订阅 webpack 编译生命周期中的钩子(hooks),在合适的时机介入构建过程,能力远比 loader 强大(loader 只转换文件,plugin 可操作整个编译)。

javascriptCode
// 自定义 plugin:构建后生成一份资源清单
class AssetManifestPlugin {
  apply(compiler) {
    // emit 钩子:在生成资源到输出目录之前触发
    compiler.hooks.emit.tapAsync('AssetManifestPlugin', (compilation, callback) => {
      const manifest = {};
      for (const filename in compilation.assets) {
        manifest[filename] = compilation.assets[filename].size();
      }
      const json = JSON.stringify(manifest, null, 2);
      // 把清单作为一个新资源加入输出
      compilation.assets['asset-manifest.json'] = {
        source: () => json,
        size: () => json.length,
      };
      callback();
    });
  }
}

module.exports = {
  plugins: [new AssetManifestPlugin()],
};

常用 webpack 钩子:

| 钩子 | 时机 | 典型用途 |

| --- | --- | --- |

| environment | 环境准备好 | 配置初始化 |

| compile | 开始编译 | 记录开始时间 |

| compilation | 创建 compilation | 注册 compilation 级钩子 |

| emit | 输出资源前 | 修改/新增产物 |

| afterEmit | 输出资源后 | 上传 CDN、通知 |

| done | 构建完成 | 打印统计、触发部署 |

Vite 插件原理与自定义

Vite 插件基于 Rollup 插件接口,并扩展了 Vite 专属钩子(如 config、configureServer、transformIndexHtml)。

javascriptCode
// 自定义 Vite 插件:注入构建时间到 HTML
function buildTimePlugin() {
  return {
    name: 'vite-plugin-build-time',
    // 修改最终配置
    config(config) {
      return { define: { __BUILD_TIME__: JSON.stringify(new Date().toISOString()) } };
    },
    // 转换 index.html
    transformIndexHtml(html) {
      return html.replace('</head>', '<meta name="build-time" content="' + new Date().toISOString() + '"></head>');
    },
    // 通用 transform 钩子(Rollup 兼容)
    transform(code, id) {
      if (id.endsWith('.special.js')) {
        return { code: code.replace(/foo/g, 'bar'), map: null };
      }
    },
  };
}

export default defineConfig({
  plugins: [buildTimePlugin()],
});

Vite 插件钩子执行顺序(enforce):

| enforce | 时机 | 用途 |

| --- | --- | --- |

| pre | 核心插件之前 | 需最先处理(如别名) |

| (默认) | 核心插件中 | 常规转换 |

| post | 核心插件之后 | 产物最终处理 |

resolve 解析配置深入

javascriptCode
module.exports = {
  resolve: {
    // 省略后缀(按顺序尝试)——列得越少解析越快
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
    // 路径别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      // 强制统一某个库的版本,避免重复打包
      react: path.resolve(__dirname, 'node_modules/react'),
    },
    // 决定优先用包的哪个入口字段
    mainFields: ['browser', 'module', 'main'],
    // 减少向上查找 node_modules 的层级,提升解析速度
    modules: [path.resolve(__dirname, 'node_modules'), 'node_modules'],
    // 对称链接的处理(monorepo 常用)
    symlinks: false,
  },
};

优化解析速度的要点:

extensions 只列必要后缀,且高频在前。
用 alias 直连高频目录,减少查找。
明确 modules 路径,避免逐级向上找。
导入时尽量写完整后缀(如 './a.js'),省去猜测。

externals 与 CDN 外链

把大型稳定依赖交给 CDN,不打进 bundle,减小产物、利用公共缓存。

javascriptCode
module.exports = {
  // 声明这些依赖由外部(CDN 全局变量)提供
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_',
  },
};

// HTML 中通过 CDN 引入
// <script src="https://cdn.example.com/react@18/umd/react.production.min.js"></script>
// <script src="https://cdn.example.com/react-dom@18/umd/react-dom.production.min.js"></script>

externals 权衡:

| 优点 | 缺点 |

| --- | --- |

| 减小 bundle 体积 | 依赖 CDN 可用性 |

| 利用公共缓存 | 版本需手动对齐 |

| 并行下载 | 无法 Tree Shaking CDN 库 |

| 加快构建 | 多一次外部请求 |

多环境配置管理

javascriptCode
// webpack.common.js —— 公共配置
const common = {
  entry: './src/index.js',
  module: { /* 公共 loader */ },
  plugins: [ /* 公共 plugin */ ],
};

// webpack.dev.js —— 开发配置
const { merge } = require('webpack-merge');
module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: { hot: true, port: 3000 },
});

// webpack.prod.js —— 生产配置
module.exports = merge(common, {
  mode: 'production',
  devtool: 'hidden-source-map',
  optimization: { minimize: true, splitChunks: { chunks: 'all' } },
});

// package.json scripts
// "dev": "webpack serve --config webpack.dev.js",
// "build": "webpack --config webpack.prod.js"
javascriptCode
// Vite 多环境:用 mode 与 .env 文件
// .env.development / .env.production / .env.staging
// VITE_API_URL=https://api.example.com

// vite.config.js 根据 mode 动态配置
export default defineConfig(({ mode, command }) => {
  const isProd = mode === 'production';
  return {
    build: { sourcemap: !isProd, minify: isProd ? 'esbuild' : false },
  };
});

类型检查与 Lint 的并行化

esbuild/SWC 不做类型检查以换取速度,因此需要把 tsc 类型检查独立并行运行,避免阻塞构建。

javascriptCode
// 用 fork-ts-checker-webpack-plugin 把类型检查放到独立进程,不阻塞打包
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: 'esbuild-loader', // 只转译,不检查类型
          options: { target: 'es2015' },
        },
      },
    ],
  },
  plugins: [
    new ForkTsCheckerWebpackPlugin({
      typescript: { diagnosticOptions: { semantic: true, syntactic: true } },
    }),
  ],
};
jsonCode
// package.json —— CI 中并行跑构建、类型检查、lint
{
  "scripts": {
    "build": "vite build",
    "type-check": "tsc --noEmit",
    "lint": "eslint src --ext .ts,.tsx",
    "ci": "npm-run-all --parallel type-check lint build"
  }
}

增量类型检查

jsonCode
// tsconfig.json —— 开启 incremental,缓存类型检查结果
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo",
    "composite": true
  }
}

Project References(项目引用)让 monorepo 中的 tsc 只重新检查变化的子项目,大型代码库类型检查从"分钟级"降到"秒级"。

Web Worker 与 WASM

javascriptCode
// Vite 原生支持 Worker 导入
import MyWorker from './heavy.worker.js?worker';
const worker = new MyWorker();
worker.postMessage({ data: bigArray });
worker.onmessage = (e) => console.log('结果', e.data);

// Webpack 5 用 new Worker(new URL(...)),自动打包 worker
const worker = new Worker(new URL('./heavy.worker.js', import.meta.url));

// WASM 导入(Vite)
// import init, { compute } from './pkg/my_wasm.js';
// await init();
// const result = compute(input);

把 CPU 密集计算(图像处理、加解密、大数据排序)放到 Worker,避免阻塞主线程,是提升交互性能的重要手段。

PWA 与 Workbox

javascriptCode
// 用 workbox-webpack-plugin 生成 Service Worker,实现离线缓存
const { GenerateSW } = require('workbox-webpack-plugin');

module.exports = {
  plugins: [
    new GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          // 图片用 CacheFirst,长期缓存
          urlPattern: /\.(png|jpg|jpeg|svg|webp)$/,
          handler: 'CacheFirst',
          options: { cacheName: 'images', expiration: { maxEntries: 60 } },
        },
        {
          // API 用 NetworkFirst,优先网络、失败回退缓存
          urlPattern: /\/api\//,
          handler: 'NetworkFirst',
          options: { cacheName: 'api', networkTimeoutSeconds: 3 },
        },
      ],
    }),
  ],
};

// Vite 用 vite-plugin-pwa,配置更简洁

依赖去重与包管理器影响

javascriptCode
// 检查重复依赖
// npm ls react        —— 查看 react 被多少版本引入
// npx webpack-bundle-analyzer  —— 可视化中能看到重复模块

// 用 resolve.alias 强制统一版本
resolve: {
  alias: {
    react: path.resolve('./node_modules/react'),
    'react-dom': path.resolve('./node_modules/react-dom'),
  },
},

// package.json 用 overrides(npm)/ resolutions(yarn)强制统一
// "overrides": { "react": "18.2.0" }

包管理器对构建的影响:

| 包管理器 | node_modules 结构 | 安装速度 | 磁盘占用 | 幽灵依赖风险 |

| --- | --- | --- | --- | --- |

| npm | 扁平 | 中 | 大 | 有 |

| yarn (classic) | 扁平 | 快 | 大 | 有 |

| pnpm | 硬链接 + 符号链接 | 最快 | 最小 | 低(严格) |

pnpm 的严格 node_modules 结构能杜绝"幽灵依赖"(用了没在 package.json 声明的包),让构建更可靠、CI 更快。

真实案例四:首屏性能预算落地

背景: 一个电商首页因体积失控导致移动端首屏慢,团队引入性能预算治理。

javascriptCode
// 在 CI 中用 size-limit 卡首屏体积,超标 PR 无法合并
// size-limit.json
[
  { "path": "dist/js/main.*.js", "limit": "160 KB", "gzip": true },
  { "path": "dist/js/vendor.*.js", "limit": "120 KB", "gzip": true },
  { "path": "dist/css/*.css", "limit": "40 KB", "gzip": true }
]

治理三个月效果:

| 指标 | 治理前 | 治理后 |

| --- | --- | --- |

| 首屏 JS(gzip) | 420KB | 155KB |

| 首屏渲染(4G) | 4.8s | 1.9s |

| 转化率 | 基线 | +6.2% |

| 体积回归次数 | 频繁 | CI 自动拦截为 0 |

更多优化技巧速览

javascriptCode
// 1. IgnorePlugin:忽略不需要的模块(如 moment 的所有 locale)
const webpack = require('webpack');
new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ });

// 2. ProvidePlugin:自动注入全局变量,免去到处 import
new webpack.ProvidePlugin({ React: 'react', _: 'lodash' });

// 3. 限制 chunk 请求数,避免过度分割导致请求爆炸
optimization: { splitChunks: { maxInitialRequests: 25, minSize: 20000 } };

// 4. 用 assetsInlineLimit 控制小资源内联阈值(Vite)
build: { assetsInlineLimit: 4096 };

// 5. 关闭生产环境 reportCompressedSize 提升构建速度(Vite 大项目)
build: { reportCompressedSize: false };

// 6. 用 esbuild 压缩 CSS
build: { cssMinify: 'esbuild' };

Scope Hoisting 作用域提升

什么是 Scope Hoisting? 默认情况下 webpack 把每个模块包裹在一个函数里(module wrapper),模块多时会产生大量闭包,增大体积、拖慢执行。Scope Hoisting(作用域提升)把可以安全合并的模块拼接到同一作用域,减少函数包裹。

javascriptCode
module.exports = {
  optimization: {
    // 生产模式默认开启;也可显式声明
    concatenateModules: true,
  },
};
// 效果:更少的函数闭包、更小的产物、更快的初始化
// 前提:模块必须是 ESM(CommonJS 无法被 concatenate)

Terser 压缩深度配置

javascriptCode
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,              // 多进程并行压缩
        extractComments: false,      // 不把注释抽成单独文件
        terserOptions: {
          compress: {
            drop_console: true,      // 移除 console.*
            drop_debugger: true,     // 移除 debugger
            pure_funcs: ['console.info'], // 视为无副作用可删除
            passes: 2,               // 多轮压缩,体积更小(更慢)
          },
          mangle: { safari10: true }, // 兼容 Safari 10 的变量名混淆
          format: { comments: false },
        },
      }),
    ],
  },
};

死代码消除与常量折叠

javascriptCode
// DefinePlugin 把变量替换为常量,配合压缩器消除不可达分支
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' });

// 源码:
if (process.env.NODE_ENV !== 'production') {
  console.log('调试信息'); // 生产构建中:条件恒为 false,整块被删除
}

// 用 __DEV__ 标志包裹开发专用代码,生产自动摇掉
if (__DEV__) {
  validateProps(props);
}

CSS Modules 与样式方案

javascriptCode
// CSS Modules:类名局部作用域,避免全局污染
{
  test: /\.module\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: {
          localIdentName: '[name]__[local]--[hash:base64:5]',
        },
      },
    },
  ],
}

// 使用
// import styles from './Button.module.css';
// <button className={styles.primary}>按钮</button>

样式方案对构建的影响:

| 方案 | 运行时开销 | 构建期提取 | Tree Shaking | 适用 |

| --- | --- | --- | --- | --- |

| 普通 CSS | 无 | 是 | 弱 | 简单项目 |

| CSS Modules | 无 | 是 | 中 | 中大型 |

| Sass/Less | 无 | 是 | 中 | 需预处理 |

| CSS-in-JS(运行时) | 有 | 否 | 好 | 组件化强 |

| 原子化(Tailwind) | 无 | 是(需 purge) | 极好 | 快速开发 |

| 零运行时 CSS-in-JS | 无 | 是 | 好 | 追求极致 |

Asset Modules 资源模块

Webpack 5 用内置 Asset Modules 取代了 file-loader / url-loader / raw-loader。

javascriptCode
module.exports = {
  module: {
    rules: [
      // asset/resource:输出单独文件(相当于 file-loader)
      { test: /\.(png|jpg)$/, type: 'asset/resource' },
      // asset/inline:转 base64 内联(相当于 url-loader)
      { test: /\.svg$/, type: 'asset/inline' },
      // asset/source:导出源码字符串(相当于 raw-loader)
      { test: /\.txt$/, type: 'asset/source' },
      // asset:自动在 resource 与 inline 之间选择
      {
        test: /\.(woff2?|eot|ttf)$/,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
      },
    ],
  },
};

服务端缓存与压缩配置

构建产物的价值需要服务端配合正确的 HTTP 缓存与压缩才能完全释放。

nginxCode
# nginx —— 带 contenthash 的静态资源用强缓存 + 长过期
location ~* \.(js|css|png|jpg|webp|woff2)$ {
  # 内容哈希文件名可安全长期缓存
  expires 1y;
  add_header Cache-Control "public, immutable";
}

# HTML 不缓存或短缓存,保证能拿到最新的资源引用
location / {
  add_header Cache-Control "no-cache";
}

# 优先返回预压缩的 br / gz 文件
gzip_static on;
brotli_static on;

缓存策略对照:

| 资源类型 | Cache-Control | 原因 |

| --- | --- | --- |

| 带 hash 的 JS/CSS | public, immutable, max-age=1y | 内容变则文件名变 |

| 图片/字体(带 hash) | public, max-age=1y | 同上 |

| HTML | no-cache | 需引用最新资源 |

| API | no-store 或按需 | 数据实时性 |

资源提示:preload / prefetch / preconnect

htmlCode
<!-- preconnect:提前建立到关键第三方域名的连接(DNS+TCP+TLS) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="https://api.example.com">

<!-- preload:当前页一定会用到的关键资源,高优先级 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/js/critical.js" as="script">

<!-- prefetch:未来导航可能用到,空闲时下载,低优先级 -->
<link rel="prefetch" href="/js/next-page.js" as="script">

<!-- modulepreload:预加载 ES 模块及其依赖 -->
<link rel="modulepreload" href="/js/app.js">

分析 stats.json

javascriptCode
// 生成详细的构建统计,供工具深度分析
// webpack --json > stats.json
// 然后上传到 https://webpack.github.io/analyse/ 或用 webpack-bundle-analyzer

module.exports = {
  stats: {
    assets: true,
    chunks: true,
    modules: false,     // 模块太多可关闭
    reasons: true,      // 显示模块被谁引入(排查为何被打包)
    chunkModules: true,
  },
};

stats 的 reasons 字段能回答"为什么这个大库被打进来了"——顺着引用链就能找到意外的依赖引入。

Dev Server 高级配置

javascriptCode
module.exports = {
  devServer: {
    hot: true,
    port: 3000,
    open: true,
    // SPA 路由回退,刷新子路由不 404
    historyApiFallback: true,
    // 允许局域网访问(手机真机调试)
    host: '0.0.0.0',
    // 代理配置(解决开发跨域)
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
        // 转发 WebSocket
        ws: true,
      },
    },
    // 错误覆盖层
    client: {
      overlay: { errors: true, warnings: false },
      progress: true,
    },
    // 压缩响应
    compress: true,
  },
};

HMR 常见故障排查

| 现象 | 可能原因 | 排查/解决 |

| --- | --- | --- |

| 改代码整页刷新 | 无 accept 边界 / 框架插件缺失 | 装 react-refresh / vue 插件 |

| HMR 不生效 | WebSocket 被代理拦截 | 配置 proxy ws:true,检查网络 |

| 状态丢失 | 组件被完全替换 | 用 Fast Refresh,避免匿名导出 |

| 样式不热更 | style-loader 缺失或用了 extract | 开发环境用 style-loader |

| 更新很慢 | 项目大 + webpack | 换 Vite / 缩小监听范围 |

术语速查表

| 术语 | 含义 |

| --- | --- |

| Bundle | 打包后的输出文件 |

| Chunk | 代码分割产生的代码块 |

| Module | 源码中的单个文件/模块 |

| Loader | 转换单个文件内容的函数 |

| Plugin | 介入构建生命周期的扩展 |

| Tree Shaking | 消除未使用的导出 |

| Code Splitting | 按需拆分代码 |

| HMR | 热模块替换 |

| Scope Hoisting | 作用域提升,合并模块 |

| Source Map | 压缩代码到源码的映射 |

| contenthash | 基于文件内容的指纹 |

| Externals | 由外部提供、不打包的依赖 |

| sideEffects | 声明模块是否有副作用 |

真实案例五:Rspack 迁移提速

背景: 一个 200 人协作的大型 React 应用,webpack 冷构建慢到影响本地开发。

迁移: 由于 Rspack 高度兼容 webpack 配置,团队仅用两天完成迁移(主要调整少数不兼容插件)。

| 指标 | Webpack | Rspack | 提升 |

| --- | --- | --- | --- |

| 开发冷启动 | 88s | 8s | 91% |

| 生产构建 | 5.5 分钟 | 55s | 83% |

| HMR | 2.1s | 200ms | 90% |

| 配置改动 | - | 约 30 行 | 低成本 |

优化实施 checklist

按此清单逐项落地,能覆盖绝大多数项目的构建优化需求:

[ ] 确认源码与依赖尽量使用 ESM。
[ ] Babel 设 modules: false,保证 Tree Shaking。
[ ] package.json 正确声明 sideEffects。
[ ] 路由与重型模块用动态 import 分割。
[ ] splitChunks 抽离 framework/vendor/common。
[ ] output 用 contenthash,moduleIds 用 deterministic,runtimeChunk single。
[ ] 开启 filesystem 缓存与 loader 缓存。
[ ] 用 esbuild/SWC 替代 babel 转译与压缩。
[ ] loader 限定 include,跳过 node_modules。
[ ] 按需引入工具库,moment 换 dayjs。
[ ] 图片用 WebP/AVIF,字体子集化。
[ ] 开启 gzip/brotli 压缩,服务端配合。
[ ] 设置性能预算并接入 CI。
[ ] source map 生产用 hidden/nosources。
[ ] 类型检查独立进程并行运行。
[ ] 静态资源配置长期强缓存,HTML 不缓存。

动态 publicPath 与多环境部署

javascriptCode
// 构建期固定 publicPath
module.exports = {
  output: { publicPath: '/static/' },
};

// 运行时动态设置(同一份产物部署到不同 CDN 路径)
// 在入口最顶部:
__webpack_public_path__ = window.__CDN_BASE__ || '/';

国际化资源按需加载

javascriptCode
// 语言包动态导入,只加载当前语言,不打包全部
async function loadLocale(lang) {
  const messages = await import(
    /* webpackChunkName: "locale-[request]" */
    './locales/' + lang + '.json'
  );
  i18n.setLocaleMessage(lang, messages.default);
}
// 首屏只下载一种语言包,切换时再按需拉取其它语言

图标按需与 SVG Sprite

javascriptCode
// ❌ 全量引入图标库,打进上千个用不到的图标
import * as Icons from '@some/icons';

// ✅ 按需具名引入,配合 Tree Shaking 只打包用到的
import { SearchIcon, CloseIcon } from '@some/icons';

// SVG Sprite:把多个 SVG 合并成雪碧图,减少请求
// svg-sprite-loader
{
  test: /\.svg$/,
  include: path.resolve(__dirname, 'src/icons'),
  use: [{ loader: 'svg-sprite-loader', options: { symbolId: 'icon-[name]' } }],
}
// 使用:<svg><use xlink:href="#icon-search" /></svg>

用 Preact 替换 React 减小体积

javascriptCode
// 通过 alias 把 react 指向 preact/compat,体积从约 45KB 降到约 10KB
module.exports = {
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
};
// 注意:需充分测试兼容性,某些依赖 React 内部 API 的库可能不工作

Next.js 中的 webpack 定制

javascriptCode
// next.config.js —— 在框架封装之上做定制
module.exports = {
  // 自定义 webpack(谨慎,尽量用框架提供的能力)
  webpack: (config, { dev, isServer }) => {
    // 生产客户端构建才做的处理
    if (!dev && !isServer) {
      config.optimization.splitChunks.cacheGroups.commons = {
        name: 'commons',
        chunks: 'all',
        minChunks: 2,
      };
    }
    return config;
  },
  // 现代图片格式
  images: { formats: ['image/avif', 'image/webp'] },
  // 用 SWC 压缩(默认开启)
  swcMinify: true,
  // 实验特性:更激进的包体优化
  experimental: {
    optimizePackageImports: ['lodash-es', '@some/icons'],
  },
};

watch 选项与文件监听优化

javascriptCode
module.exports = {
  watchOptions: {
    // 忽略 node_modules,减少监听负担
    ignored: /node_modules/,
    // 聚合变化,避免频繁重建
    aggregateTimeout: 300,
    // 某些系统需要轮询才能监听(如 Docker/网络盘)
    poll: process.env.WATCH_POLL ? 1000 : false,
  },
};

抑制无关告警

javascriptCode
module.exports = {
  // Webpack 5:精确忽略已知的无害告警
  ignoreWarnings: [
    /Failed to parse source map/,
    (warning) => warning.module?.resource?.includes('some-lib'),
  ],
};

Monorepo 内部包的构建策略

jsonCode
// 内部包直接导出源码(TS),由使用方一起编译,省去中间构建
// packages/ui/package.json
{
  "name": "@repo/ui",
  "main": "./src/index.ts",
  "exports": { ".": "./src/index.ts" }
}
// 配合应用侧 transpilePackages(Next.js)或 include 编译内部包

对内部包,"源码直出 + 应用统一编译"避免了每个包单独 build 的开销,配合 Turborepo 缓存效果更佳;对外发布的包才需要独立构建产物。

真实案例六:依赖体积治理

背景: 某项目 bundle-analyzer 显示 vendor 高达 1.8MB,逐项治理。

| 问题依赖 | 原体积 | 处理 | 处理后 |

| --- | --- | --- | --- |

| moment + locale | 290KB | 换 dayjs | 12KB |

| lodash 全量 | 71KB | lodash-es 按需 | 6KB |

| 全量 antd icons | 480KB | 按需引入 | 40KB |

| 两个版本的 core-js | 160KB | overrides 统一 | 85KB |

| 重复的 react | 90KB | alias 去重 | 45KB |

| 未压缩 SVG 组件 | 120KB | sprite + 优化 | 30KB |

| 合计 vendor | 1.8MB | - | 约 560KB |

结论: 体积治理最有效的三板斧是"换轻量替代品、按需引入、去重",往往比调整打包配置收益更大。

优化前后综合数据回顾

汇总本文各案例,构建优化的典型收益区间:

| 优化目标 | 常见提升幅度 | 主要手段 |

| --- | --- | --- |

| 开发冷启动 | 85%~96% | Vite/Rspack、SWC、缓存 |

| 生产构建时间 | 60%~85% | 缓存、并行、esbuild 压缩 |

| HMR 速度 | 90%+ | Vite / Fast Refresh |

| 首屏 JS 体积 | 50%~80% | 分割、Tree Shaking、按需 |

| 传输体积 | 85%~90% | gzip/brotli |

| 二次访问命中率 | 提升至 90%+ | contenthash 缓存策略 |

| CI 构建(命中缓存) | 80%+ | Turborepo/Nx、CI 缓存 |

构建可观测性与监控

把构建指标持续采集、可视化,才能及早发现劣化趋势,而不是等到"某天突然很慢"。

javascriptCode
// 自定义 plugin:把每次构建的关键指标上报到监控系统
class BuildMetricsPlugin {
  apply(compiler) {
    let start;
    compiler.hooks.compile.tap('BuildMetrics', () => { start = Date.now(); });
    compiler.hooks.done.tap('BuildMetrics', (stats) => {
      const { assets } = stats.toJson({ assets: true });
      const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
      const metrics = {
        duration: Date.now() - start,
        totalSize,
        assetCount: assets.length,
        errors: stats.hasErrors(),
        timestamp: Date.now(),
        branch: process.env.CI_BRANCH,
      };
      // 上报到监控平台(Grafana/Datadog/自建)
      reportToMonitoring(metrics);
    });
  }
}

值得长期追踪的构建指标:

| 指标 | 含义 | 告警建议 |

| --- | --- | --- |

| 构建时长 | 端到端耗时 | 环比上升 20% 告警 |

| 产物总体积 | 所有资源之和 | 超预算告警 |

| 首屏 chunk 体积 | 入口 + 关键 chunk | 超预算阻断 |

| chunk 数量 | 分割粒度 | 异常增多需排查 |

| 缓存命中率 | CI 缓存命中比例 | 持续下降需排查 |

常见问题 FAQ

Q:为什么本地构建快,CI 却很慢?

CI 通常没有本地的持久化缓存、机器配置更低、还要重新装依赖。对策:缓存 node_modules 与构建缓存目录、用 npm ci、选更高配 runner、用增量构建。

Q:Tree Shaking 明明配了却没生效?

最常见是 Babel 把 ESM 转成了 CommonJS(modules 没设 false),或依赖没提供 ESM 入口,或 sideEffects 未声明。用 bundle-analyzer 确认,逐一排查。

Q:改一行代码为什么所有文件 hash 都变了?

moduleIds/chunkIds 未用 deterministic,或 runtimeChunk 没抽出,导致模块 ID 变化波及全部。设 moduleIds: 'deterministic' + runtimeChunk: 'single'。

Q:Vite 开发时频繁"重新加载/预构建"?

通常是新依赖被动态发现触发二次预构建。把 CJS/大依赖显式写进 optimizeDeps.include,可避免。

Q:生产用了 source map 会泄露源码吗?

用 source-map 会。改用 hidden-source-map(生成但 HTML 不引用,仅上传错误监控平台)或 nosources-source-map(不含源码内容)。

Q:要不要上 esbuild/SWC?

构建慢且瓶颈在转译/压缩时非常值得。注意 esbuild 不做类型检查,需配合 tsc 独立并行;个别老语法/装饰器支持有限,需验证。

未来趋势

Rust 工具链崛起:Rspack、Turbopack、SWC、Oxc、Lightning CSS 等 Rust/Go 工具将逐步成为主流,构建速度进入新量级。
无打包(Bundleless)开发:基于原生 ESM 的开发模式(Vite 引领)持续普及,冷启动趋近于零。
更智能的按需:框架层自动分析、自动分割、自动预取(如 React Server Components、部分水合)。
统一工具链:Biome、Oxc 等尝试用单一 Rust 工具统一 lint/format/transform/bundle,减少工具碎片。
构建缓存云端化:远程缓存(Turborepo Remote Cache、Nx Cloud)让团队共享构建产物,人人受益。

理解这些趋势,能帮助团队在选型时做出更具前瞻性的决策,避免频繁推倒重来。

构建安全

构建环节也是供应链安全的重要一环,需要防范恶意依赖、密钥泄露与产物篡改。

javascriptCode
// 1. 依赖审计:CI 中检查已知漏洞
// npm audit --audit-level=high
// pnpm audit

// 2. 锁定依赖版本,用 lockfile 保证可复现
// npm ci 严格按 package-lock.json 安装

// 3. 防止密钥被打进产物:只暴露白名单前缀的环境变量
new webpack.DefinePlugin({
  // ❌ 不要直接 JSON.stringify(process.env),会泄露所有服务端密钥
  // ✅ 只显式声明需要暴露给客户端的
  'process.env.PUBLIC_API_URL': JSON.stringify(process.env.PUBLIC_API_URL),
});

// 4. 子资源完整性(SRI):防止 CDN 资源被篡改
// webpack-subresource-integrity 插件为产物生成 integrity 哈希

构建安全清单:

| 风险 | 防范措施 |

| --- | --- |

| 恶意/漏洞依赖 | npm audit、锁版本、最小依赖 |

| 密钥泄露 | 只暴露白名单变量,扫描产物 |

| CDN 篡改 | SRI 完整性校验 |

| 供应链投毒 | 锁 lockfile、审查新依赖 |

| source map 泄露源码 | 生产用 hidden/nosources |

依赖体积预防机制

治理之后更要预防,避免体积反弹。

javascriptCode
// 用 bundlesize / size-limit 在 CI 卡关(前文已述)
// 再配合 import 成本可视化,让开发写代码时就意识到体积

// 1. 编辑器插件 Import Cost:实时显示每个 import 的体积
// 2. CI 中对比 PR 前后的 bundle 差异(如 relative-ci / bundlewatch)
// 3. 定期跑 bundle-analyzer 并归档,观察趋势

收尾:优化的优先级思路

面对一个待优化的项目,建议按"投入产出比"排序处理:

1.先量化现状:跑 bundle-analyzer、speed-measure、Lighthouse,用数据说话。
2.摘低垂果实:换 dayjs、按需引入、开 gzip/brotli、加 contenthash——成本低、收益大。
3.提构建速度:换 esbuild/SWC、开缓存、限定 include——改善开发体验。
4.做代码分割:路由懒加载、抽 vendor——改善首屏。
5.建长效机制:性能预算 + CI 卡关 + 构建监控——防止劣化。
6.视需要选型升级:新项目上 Vite,老项目提速考虑 Rspack。

优化不是一次性工程,而是"测量-优化-验证-守护"的持续循环。把可量化的目标写进 CI,让性能成为团队的共识与纪律,才能在快速迭代中长期保持良好的构建效率与产物质量。

构建工具的演进史

理解工具的来龙去脉,有助于把握其设计取舍与适用边界。

| 阶段 | 代表工具 | 解决的问题 | 局限 |

| --- | --- | --- | --- |

| 手工时代 | 手动 script 标签 | 无 | 依赖管理靠人、全局污染 |

| 任务运行器 | Grunt / Gulp | 自动化压缩/合并 | 不理解模块依赖 |

| 模块打包器 | Browserify / Webpack | 模块化 + 依赖图 | 配置复杂、构建慢 |

| 库打包器 | Rollup | 极致 Tree Shaking | 应用场景弱 |

| 极速转译 | esbuild / SWC | 转译/压缩慢 | 类型检查、生态 |

| 无打包开发 | Vite | 冷启动慢、HMR 慢 | 生产仍需打包 |

| Rust 全家桶 | Rspack / Turbopack / Oxc | webpack 太慢 | 生态仍在完善 |

从这条脉络能看出一条主线:每一代工具都在解决上一代"太慢、太重、配置太复杂"的痛点,而核心的模块化、依赖图、Tree Shaking、代码分割等概念一脉相承。掌握这些底层概念,无论工具怎么变,都能快速上手。

javascriptCode
// 无论用哪个工具,核心心智模型都是这套:
// 1. 入口(entry)
// 2. 依赖图(module graph)—— 从入口出发解析所有 import
// 3. 转换(transform)—— loader/plugin 处理各类文件
// 4. 优化(optimize)—— Tree Shaking / 分割 / 压缩
// 5. 输出(output)—— 带指纹的 bundle/chunk/asset
// 记住这五步,任何构建工具的配置都能对号入座

一句话收尾:构建优化的终极目标,是让开发者写代码时快、让用户加载页面时也快。 前者靠更快的工具链与缓存,后者靠更小的产物与更聪明的加载策略。用数据驱动、建立长效机制,就能在两者之间持续取得最佳平衡。

分析首屏加载瀑布的实战方法

优化首屏,先看清资源加载的时间线,找到"阻塞点"与"发现太晚"的资源。

javascriptCode
// 用 Performance API 采集资源加载耗时,定位慢资源
performance.getEntriesByType('resource')
  .filter((e) => e.initiatorType === 'script' || e.initiatorType === 'css')
  .map((e) => ({
    name: e.name.split('/').pop(),
    duration: Math.round(e.duration),
    size: e.transferSize,
    // 阻塞时间:从发现到开始下载的等待
    blocked: Math.round(e.startTime),
  }))
  .sort((a, b) => b.duration - a.duration)
  .forEach((e) => console.log(e.name, e.duration + 'ms', (e.size / 1024).toFixed(1) + 'KB'));

首屏优化的三个层次:

| 层次 | 目标 | 手段 |

| --- | --- | --- |

| 更少 | 减少首屏字节 | 分割、Tree Shaking、按需 |

| 更早 | 尽早发现关键资源 | preload、真实标签而非 JS 注入 |

| 更快 | 加快单资源下载 | CDN、压缩、现代格式、HTTP/2/3 |

结合构建配置(分割、压缩、指纹)与资源提示(preload/preconnect),三管齐下才能把首屏做到极致。构建工具输出的是"更小、更可缓存的产物",而如何加载这些产物,同样是首屏性能的关键一环——二者缺一不可。

团队协作层面的构建规范

工具与配置之外,团队约定同样决定构建的长期健康度。

统一包管理器与 Node 版本:用 .nvmrc、engines 字段与 packageManager 字段锁定,避免"在我机器上能构建"。
配置集中管理:把 webpack/vite 配置抽成可复用预设,团队共享,减少各项目各自为战。
新增依赖需评审:引入大依赖前先看 bundlephobia 体积、维护活跃度、许可证。
构建告警纳入日常:把构建时长、体积趋势放到团队看板,异常及时处理。
文档化优化决策:为什么用某个 splitChunks 策略、为什么 external 某个库,写清楚,避免后人误改。
jsonCode
// package.json —— 锁定工具链,保证团队一致
{
  "packageManager": "pnpm@9.0.0",
  "engines": { "node": ">=20.0.0" }
}

规范落到工具与流程里,才能真正长期生效——否则再好的优化也会随着人员流动、需求堆积而慢慢劣化。让构建优化成为团队工程文化的一部分,是所有技术手段能持续见效的根本保障。

最佳实践总结

选择构建工具时,需要根据项目的具体需求和团队的技术栈进行权衡。Webpack适合复杂的企业级应用,Vite适合现代前端项目,Rollup适合库开发,Parcel适合小型项目。无论选择哪种工具,都应该关注构建性能优化、代码分割、缓存策略等方面,以提升开发体验和用户体验。

构建性能优化

通用策略:

减少依赖体积
按需加载
缓存构建结果
并行构建
优化构建配置

依赖管理:

分析依赖体积:webpack-bundle-analyzer
移除未使用的依赖
合理使用 polyfill
按需引入库

构建缓存:

缓存依赖:node_modules/.cache
缓存构建结果:webpack 缓存
缓存 CI/CD 构建:actions/cache
合理的缓存策略

监控与分析:

构建时间监控
构建产物分析
性能瓶颈识别
持续优化

最佳实践

开发环境:

使用 Vite 或 webpack-dev-server
启用热模块替换
优化开发服务器配置
合理的源码映射

生产环境:

代码分割
资源压缩
缓存策略
按需加载

CI/CD 环境:

缓存依赖
并行构建
构建产物分析
性能监控

工具与资源

分析工具:

webpack-bundle-analyzer
rollup-plugin-visualizer
source-map-explorer
bundlephobia

配置工具:

create-vite
vue-cli
create-react-app
custom webpack config

学习资源:

Webpack 文档
Vite 文档
Rollup 文档
前端构建优化指南

案例分析

大型项目构建优化:

Facebook 的构建优化
Google 的构建优化
阿里巴巴的构建优化
腾讯的构建优化

实施效果:

开发服务器启动时间减少
构建时间缩短
构建产物体积减小
部署速度提升

结语

构建工具优化是一项贯穿项目全生命周期的系统工程,它既涉及对工具原理(模块图、Tree Shaking、代码分割、缓存指纹)的深入理解,也需要工程化的纪律(性能预算、CI 卡关、持续监控)。本文从工具对比、Webpack/Vite/Rollup 深度优化,到转译提速、体积治理、微前端、Monorepo 缓存、构建安全与团队规范,覆盖了从原理到落地的完整链路。希望读者能以"测量-优化-验证-守护"的闭环思路,结合自身项目实际,选择合适的工具与策略,让开发更高效、产物更精简、用户体验更流畅。

核心要点回顾:

原理为纲:入口、依赖图、转换、优化、输出五步心智模型,是理解一切构建工具的钥匙。
速度靠工具与缓存:Rust/Go 工具链(esbuild/SWC/Rspack/Vite)+ 持久化缓存,把开发与构建速度带入新量级。
体积靠分割与治理:代码分割、Tree Shaking、按需引入、现代格式与压缩,层层削减产物。
稳定靠指纹与预算:contenthash 长期缓存 + 性能预算 CI 卡关 + 构建监控,防止性能悄然劣化。
长效靠文化:把优化沉淀为团队规范与工具约束,才能抵抗时间与人员流动带来的熵增。
前瞻靠趋势:关注 Rust 工具链、Bundleless、云端缓存与统一工具链,为下一次选型储备判断力。

技术在变,工具在换,但"让开发者与用户都更快"这个目标始终不变。带着这个目标,用数据说话、用机制守护,构建优化就不再是一次性的救火,而是持续为团队与用户创造价值的长期投资。