Webpack系列-构建性能优化实战:从开发到生产

构建速度慢,是每一位前端开发者在使用 webpack 时都可能遇到的"心头之痛"。每次保存都要等上好几秒,热更新也变成了"热等待",这不仅影响开发体验,更在无情地吞噬我们的高效编码时间。本文将从开发和生产两个维度,深入剖析 webpack 构建性能优化的原理与实战,助你彻底告别构建等待!

为什么你的 webpack 构建这么慢?

在开始优化之前,我们首先需要建立一个核心认知:优化不是一蹴而就的,它是一个持续分析和改进的过程。 不同的项目瓶颈各不相同,盲目套用配置可能收效甚微。 构建速度主要消耗在以下几个方面:

  • 模块解析和转换 :特别是 node_modules 中的庞大库文件和复杂的 JSX/Babel 转译。
  • 插件/加载器执行:某些插件或加载器的执行逻辑本身就很耗时。
  • 输出文件生成:文件数量多、体积大,会导致写磁盘操作变慢。

接下来,我们将兵分两路,针对开发环境 (追求更快的启动速度和热更新)和生产环境(追求更小的打包体积和更快的构建流水线)分别制定优化策略。

开发环境的优化

开发环境的终极目标是:更快的启动速度(build)和更快的二次编译速度(rebuild

1. 设置mode为development

Webpackmode分为nonedevelopmentproduction

  • noneWebpack将不会采用任何优化手段
  • developmentWebpack按照开发环境特点进行默认优化
  • productionWebpack按照生产环境特点进行默认优化

mode设置为development,默认情况下将optimization选项不会开启代码压缩、作用域提升等优化。将能极大 提升构建速度

2. 添加缓存缓存

Webpack5带来了强大的持久化缓存,开发环境下非常有效的优化手段。

js 复制代码
// webpack.config.js
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件系统缓存
    buildDependencies: {
      config: [__filename], // 当 webpack 配置文件发生变化时,缓存失效
    },
    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 缓存存放位置
  },
};

💡提示

对于Webpack4项目,可以采用cache-loader,但是性能上不如Webpack5的内置缓存

3. 缩小模块范围

通过resolve选项告知Webpack在解析模块时,减少不必要的查找,可以节省大量时间。

js 复制代码
// webpack.config.js
module.exports = {
  // ...
  resolve: {
    // 优先使用 ES6 模块化语法的文件
    mainFields: ['browser', 'module', 'main'],
    // 准确配置扩展名,减少尝试
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    // 使用别名,直接指向确切路径,避免递归查找
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'react-dom': '@hot-loader/react-dom', // 例如,使用热更新增强的 react-dom
    },
    // 限制在指定目录内查找,排除 node_modules 的层层递归(慎用,确保你的模块都在这里)
    // modules: [path.resolve(__dirname, 'src'), 'node_modules']
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        // 使用 include 精确指定需要处理的目录,排除 node_modules
        include: path.resolve(__dirname, 'src'),
        use: 'babel-loader'
      }
    ]
  }
};

4. 使用更快的loader或轻量级插件

esbuild-loader/swc-loader替代babel-loader

  • esbuild-loader/swc-loader替代babel-loader进行转译,因为它们由Go/Rust语言编写,速度极快。如果项目简单,建议采用esbuild-loader,如果项目需要更加接近babel并能更加完善的兼容的话,建议采用swc-loader。具体用法如下:
在项目根目录下添加.swcrc文件
json 复制代码
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": true,
      "dynamicImport": true,
      "numericSeparator": true,
      "classPrivateProperty": true,
      "classPrivateMethod": true,
      "classProperty": true,
      "functionBind": true,
      "exportDefaultFrom": true,
      "exportNamespaceFrom": true
    },
    "transform": {
      "react": {
        "runtime": "automatic",
        "refresh": true
      }
    },
    "externalHelpers": false,
    "keepClassNames": false,
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "env": {
    "mode": "usage",
    "coreJs": 3,
    "targets": {
      "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false
}
项目根目录添加.browserslistrc文件
text 复制代码
[production]
> 0.5%
last 2 versions
Firefox ESR
not dead
not ie 11

[development]
last 1 chrome version
last 1 firefox version
last 1 safari version

SWC会自动读取.browserslistrc,根据不同环境获取对应配置。

移除不必要的插件

在开发环境中有些插件是没有必要的,例如MiniCssExtractPlugin改用style-loader。因为MiniCssExtractPlugin不支持HMR。

5. devServer优化

devSever优化主要是减少控制台的输出内容

js 复制代码
module.exports = {
  devServer: {
    stats: 'minimal' // 减少控制台输出,从而提高re-build性能
  }
}

生产环境优化

生产环境的核心目标是:减少最终打包体积,并提升构建流水线的速度

1. 采用多进程/多实例并行构建

将耗时的加载器操作并行化,充分利用多核 CPU。可以采用thread-loader实现,将它放入其他加载器之前,其后的加载器会放入到worker池中运行。

js 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        include: path.resolve(__dirname, 'src'),
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1, // CPU 核心数 -1
              poolTimeout: Infinity, // 防止进程退出
            },
          },
          'babel-loader',
        ],
      },
    ],
  },
};

📢 注意

进程启动和通信有一定开销,在非常小的项目中可能达不到提高构建提速效果,反而会变慢

2. 分包优化

合理的分包可以充分利用浏览器缓存,虽然可能略微增加构建时间,但对长期缓存收益巨大。主要从几个方面考虑:

  • 第三方库分离 :将node_modules依赖进行提取,利用缓存,减少重复打包
  • 公共模块提取 提取重复引用多次的模块,减少代码体积
  • 动态导入语懒加载 使用import()动态导入/路由懒加载,按需加载,加速首屏
js 复制代码
// JS引入模块
import(/* webpackChunkName: "my-chunk-name" */ './module');
// React 路由懒加载
import React, { Suspense, lazy } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  );
}
// Vue路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('./views/Home.vue') // 访问/home时才会加载
  }
];
  • 运行时代码分离 避免频繁变更代码影响到库缓存
js 复制代码
optimization: {
  // 提取运行时代码
  runtimeChunk: {
    name: "manifest",
  },
  splitChunks: {
    chunks: "all",
    minSize: 20000,
    minChunks: 1,
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: "vendors",
        chunks: "all",
        priority: 20, // 优先级高
      },
      common: {
        name: "common",
        minChunks: 2, // 被两个及以上chunk引用才提取
        chunks: "all",
        priority: 10,
        reuseExistingChunk: true, // 避免模块重复打包
      },
    },
  },
},

3. 采用各种工具进行压缩

Terser多进程并行压缩

js 复制代码
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true, // 开启多进程并行
        // terserOptions: { ... }
      }),
    ],
  },
};

css-minimizer-webpack-plugin插件对CSS进行压缩

js 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        // 使用 MiniCssExtractPlugin.loader 替换 style-loader
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  optimization: {
    minimizer: [
      '...', // 这表示继承 Webpack 默认的 JS 压缩器(如Terser)
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
  ],
};

如果是大型项目的话,可以开启多进程并行进行压缩从而提高构建速度。

js 复制代码
new CssMinimizerPlugin({
  parallel: true, // 启用并行压缩
  // parallel: 4, // 也可以指定具体的进程数量
})

image-minimizer-webpack-plugin

  • 安装依赖
bash 复制代码
npm i -D image-minimizer-webpack-plugin sharp imagemin-sharp
js 复制代码
new ImageMinimizerPlugin({
  minimizer: {
    implementation: ImageMinimizerPlugin.sharpMinify,
    options: {
      encodeOptions: {
        mozjpeg: {
          quality: 80,
        },
        pngquant: {
          quality: [0.8, 0.9],
        },
        webp: {
          quality: 80,
        },
      },
    },
  },
}),

4. 精确匹配externals

对于一些通过 CDN 引入的巨型库(如 echarts, three.js),可以配置 externals 将其排除在打包范围之外。

js 复制代码
// webpack.config.js
module.exports = {
  externals: {
    echarts: 'echarts',
  },
};
// 然后在 index.html 中通过 <script> 标签引入 CDN 链接

5. Tree Shaking

Tree Shaking 就是 Webpack 在打包时帮你自动删除那些未被使用的代码(也称为 "dead-code") ,这能有效减小最终打包文件的体积

合理配置sideEffects

json 复制代码
// package.json 示例
{
  "name": "your-project",
  "sideEffects": false
  // 或者,如果你有需要保留副作用的文件
  // "sideEffects": [
  //   "*.css",
  //   "src/polyfill.js"
  // ]
}

良好的编码习惯

  • 坚持使用ES6模块 因为CommonJSrequire()具有动态性,Webpack难以进行静态分析。
  • 避免导入整个库 尽量使用具名导入引入需要使用的部分

构建分析工具

Webpack Bundle Analyzer

它以可视化的树状图形式分析打包后文件的构成,帮助你发现体积过大的模块。

bash 复制代码
npm install --save-dev webpack-bundle-analyzer
js 复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server', // 启动一个本地服务器查看报告
      openAnalyzer: true, // 完成后自动打开浏览器
    })
  ]
};

通过该工具分析可以识别:

  • 较大依赖从而进行分包
  • 检查重复代码
  • 进行更加有效的代码分割

Speed-measure-webpack-plugin

主要测量各 loader 和 plugin 的执行时间,webpack5中Speed-measure-webpack-pluginmini-css-extract-plugin之间存在兼容性问题

bash 复制代码
npm i -D speed-measure-webpack-plugin

具体配置

js 复制代码
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const isProd = process.env.NODE_ENV === "production";
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin({
  outputFormat: "humanVerbose",
  // 排除影响的插件
  exclude: ["/html-webpack-plugin/", "/mini-css-extract-plugin/"],
});
const webpackConfig = {
  mode: process.env.NODE_ENV || "development",
  entry: "./src/index.js",
  devtool: isProd ? "source-map" : "eval-cheap-module-source-map",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].[contenthash:8].bundle.js",
    chunkFilename: "js/[name].[contenthash:8].chunk.js",
    clean: true,
    publicPath: isProd ? "./" : "/",
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"],
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          isProd
            ? {
                loader: MiniCssExtractPlugin.loader,
                options: {
                  publicPath: "../",
                },
              }
            : "style-loader",
          "css-loader",
        ],
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: "babel-loader",
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: "images/[name].[hash:8][ext]",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      filename: "index.html",
    }),
    ,
  ],
};
const config = smp.wrap(webpackConfig);
// 在包裹之后添加MiniCssExtractPlugin插件
config.plugins.push(
  new MiniCssExtractPlugin({
    filename: "css/[name].[contenthash:8].css",
    chunkFilename: "css/[name].[contenthash:8].chunk.css",
  })
);
module.exports = config;

构建后会在控制台中输出信息:

text 复制代码
SMP  ⏱  
General output time took 2,375 ms

 SMP  ⏱  Plugins
HtmlWebpackPlugin took 80 ms

 SMP  ⏱  Loaders
babel-loader took 409 ms
  median       = 392 ms
  mean         = 205 ms
  s.d.         = 265.166 ms
  range        = (17 ms --> 392 ms)
  module count = 2
modules with no loaders took 253 ms
  median       = 4 ms
  mean         = 6 ms
  s.d.         = 14.107 ms
  range        = (0 ms --> 145 ms)
  module count = 218
mini-css-extract-plugin, and 
css-loader took 167 ms
  median       = 164 ms
  mean         = 164 ms
  s.d.         = 0 ms
  range        = (164 ms --> 164 ms)
  module count = 2
css-loader took 54 ms
  median       = 54 ms
  mean         = 35 ms
  s.d.         = 26.87 ms
  range        = (16 ms --> 54 ms)
  module count = 2
html-webpack-plugin took 18 ms
  median       = 18 ms
  mean         = 18 ms
  range        = (18 ms --> 18 ms)
  module count = 1

通过该工具可以知道哪些loaderplugin执行时间过长,从而对它们进行更加有效的配置或者替换。

小结

Webpack 构建优化是一个系统性的工程,没有"银弹"配置可以放之四海而皆准。通过本文的探讨,我们可以将优化思路提炼为以下核心纲领:

  1. 明确目标,区别对待 :始终明确开发环境追求速度 ,生产环境兼顾速度与体积。为此采取不同的配置策略是优化的基石。
  2. 诊断先行,对症下药 :善用 webpack-bundle-analyzerspeed-measure-webpack-plugin 等分析工具,精准定位瓶颈,避免盲目优化。
  3. 拥抱缓存,减少重复:无论是 Webpack 5 的持久化缓存,还是 loader 自身的缓存,都是提升二次构建速度最有效的手段之一。
  4. 缩小范围,精准打击 :通过 includeexcluderesolve 配置等手段,让 Webpack 只处理它该处理的模块,避免无谓的搜索和转译。
  5. 善用利器,并行不悖 :利用 thread-loaderesbuildswc 等现代工具进行并行构建和高性能转译,充分利用多核 CPU 性能。
  6. 拆分有度,缓存为王:合理的代码分割(SplitChunks、动态导入)不仅能减小首包体积,更能利用浏览器缓存,带来长久的性能收益。

优化之路永无止境。最好的优化策略是结合你项目的具体规模、技术栈和团队习惯,建立持续的监控和分析机制,在迭代中不断调整和验证。希望本文能为你提供清晰的思路和实用的方法,让你和团队的开发体验"快上加快",彻底告别构建等待的烦恼!

相关推荐
Patrick_Wilson2 小时前
AI会如何评价一名前端工程师的技术人格
前端·typescript·ai编程
顾安r2 小时前
11.10 脚本算法 五子棋 「重要」
服务器·前端·javascript·游戏·flask
一枚前端小能手2 小时前
「周更第11期」实用JS库推荐:Pinia
前端·javascript·vue.js
kirinlau2 小时前
requst payload和query string parameters
前端·javascript
合作小小程序员小小店2 小时前
web网页开发,在线%就业信息管理%系统,基于idea,html,layui,java,springboot,mysql。
java·前端·spring boot·后端·intellij-idea
刘一说2 小时前
在 Web 地图上可视化遥感数据:以芜湖市为例
前端·遥感
huangql5202 小时前
Vite与Webpack完全指南:从零开始理解前端构建工具
前端·webpack·node.js
烟袅2 小时前
JavaScript 是如何“假装”多线程的?深入理解单线程与 Event Loop
前端·javascript
烟袅3 小时前
一文看懂 Promise:异步任务的“执行流程控制器”
前端·javascript