Webpack 优化技巧

FreeGuideOnline 最新 2026-07-15

javascript module: { rules: [ { test: /.js$/, use: 'babel-loader', include: [path.resolve(__dirname, 'src')], // 只处理 src 下的文件 exclude: /node_modules/, }, ]; }


- **优化 `resolve.modules` 与 `resolve.alias`**

指定 Webpack 解析模块时应当检索的目录,并利用别名避免多层相对路径的查找。

```javascript
resolve: {
  modules: [path.resolve(__dirname, 'src'), 'node_modules'], // 优先从 src 查找
  alias: {
    '@': path.resolve(__dirname, 'src'),
    utils: path.resolve(__dirname, 'src/utils/'),
  },
},
  • 减少后缀尝试

    通过 extensions 明确模块文件后缀,并尽量将高频后缀放在前面。

    resolve: {
      extensions: ['.js', '.jsx', '.json']; // 少用 .css 等,避免额外匹配
    }
    

2. 引入缓存机制

缓存能让二次构建直接复用上次结果,大幅缩短冷启动后的编译时长。

  • 开启 Babel 缓存

    babel-loader 自带 cacheDirectory 选项,会缓存转译结果到 node_modules/.cache/babel-loader

    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
        },
      },
    }
    
  • Webpack 内置持久化缓存

    Webpack 5 原生支持磁盘缓存,通过简单的配置就能实现模块和 chunk 的持久化。

    module.exports = {
      cache: {
        type: 'filesystem', // 将缓存存储到 node_modules/.cache/webpack
      },
    };
    

    开发模式下配合 cache 属性,二次启动速度极快。

3. 多进程/多实例构建

利用 CPU 多核并行处理,把耗时的 Loader 工作分配给多个子进程。

  • 使用 thread-loader

    thread-loader 放在其他 loader 之前,它会将该 loader 的工作分配到 worker 池中。

    rules: [
      {
        test: /\.js$/,
        use: ['thread-loader', 'babel-loader'],
        include: path.resolve('src'),
      },
    ];
    

    注意:启动 worker 有开销,仅当项目文件量大时才启用,切勿滥用。

  • 并行压缩插件

    terser-webpack-plugin 默认开启多进程压缩,你也可以通过 parallel 显式控制。

    const TerserPlugin = require('terser-webpack-plugin');
    optimization: {
      minimizer: [
        new TerserPlugin({
          parallel: true, // 默认值为 cpu 核心数 -1
        }),
      ],
    }
    

4. 使用 DLL 预编译(Webpack 4 及以下)

尽管 Webpack 5 内置更好的长期缓存策略,但传统项目仍考虑用 DLL 提前打包不动如山的第三方库。

  • 第一步:创建 webpack.dll.config.js

    将 Vue、React、lodash 等稳定依赖预先打包。

    const webpack = require('webpack');
    module.exports = {
      entry: {
        vendor: ['react', 'react-dom', 'lodash'],
      },
      output: {
        path: path.resolve(__dirname, 'dll'),
        filename: '[name].dll.js',
        library: '[name]_library',
      },
      plugins: [
        new webpack.DllPlugin({
          name: '[name]_library',
          path: path.resolve(__dirname, 'dll/manifest.json'),
        }),
      ],
    };
    
  • 第二步:主配置中引用 DLL

    使用 DllReferencePlugin 映射依赖,并借助 add-asset-html-webpack-plugin 自动注入 script 标签。

    plugins: [
      new webpack.DllReferencePlugin({
        manifest: require('./dll/manifest.json'),
      }),
    ];
    

    该方法在 Webpack 5 中已不推荐,可改用 HardSourceWebpackPlugin 或直接使用内置缓存。

5. 选择合适的 Source Map 模式

开发时不同 devtool 设置对编译速度影响巨大。追求速度时选 eval-cheap-module-source-map,要精确调试时选 eval-source-map

devtool: 'eval-cheap-module-source-map'; // 构建快,源码映射准确

生产环境若需定位问题,推荐 source-map (会生成独立 .map 文件) 或 hidden-source-map

二、输出质量优化

构建产物的体积直接决定了应用加载速度,优化输出是性能优化的重中之重。

1. Tree Shaking 与副作用处理

Tree Shaking 依赖于 ES Module 的静态结构,用于消除未引用的代码。

  • 确保 babel 保留 ES module 语法

    @babel/preset-env 需要设置 modules: false,否则会将 import 转成 CommonJS 导致 Tree Shaking 失效。

    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [['@babel/preset-env', { modules: false }]],
        },
      },
    }
    
  • package.json 中标记副作用

    通过 sideEffects 字段告知 Webpack 哪些文件可以安全删除未引用代码。

    {
      "sideEffects": ["*.css", "*.less"] // 防止删除包含样式导入的模块
    }
    

    设置为 false 表示所有文件均可被 Tree Shaking。

2. 代码分割(Code Splitting)

Webpack 内置三种代码分割方式:入口起点分割、动态导入分割、SplitChunksPlugin 配置分割。

  • 动态导入(按需加载)

    使用 import() 语法,Webpack 会自动将模块拆分为独立 chunk。

    button.addEventListener('click', () => {
      import('./moduleA').then((module) => {
        module.default();
      });
    });
    
  • 配置 splitChunks 提取公共模块

    将第三方库、公共业务模块提取为单独的缓存块。

    optimization: {
      splitChunks: {
        chunks: 'all', // 处理所有类型的 chunks
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            priority: 10,
            reuseExistingChunk: true,
          },
          common: {
            minChunks: 2,
            name: 'common',
            priority: 5,
            reuseExistingChunk: true,
          },
        },
      },
    }
    

    通过智能分割,浏览器能够并行加载,并利用缓存减少重复请求。

3. 压缩与优化

  • CSS 压缩

    使用 css-minimizer-webpack-plugin 在 Webpack 5 中压缩 CSS,它基于 cssnano

    const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
    optimization: {
      minimizer: [new CssMinimizerPlugin()],
    }
    

    注意:需要在 mini-css-extract-plugin 之后运行,并确保生产环境使用。

  • JavaScript 压缩

    terser-webpack-plugin 自带 dead code elimination,可移除无用代码、缩短变量名。

  • 图片与字体资源优化

    利用 image-webpack-loader 在打包时压缩图片,或 asset-modulesparser.dataUrlCondition.maxSize 将小资源转为 base64 内联。

    {
      test: /\.(png|jpe?g|gif)$/,
      type: 'asset',
      parser: {
        dataUrlCondition: {
          maxSize: 8 * 1024, // 小于8KB的文件转为base64
        },
      },
      use: [
        {
          loader: 'image-webpack-loader',
          options: { mozjpeg: { quality: 70 } },
        },
      ],
    }
    

4. Scope Hoisting(作用域提升)

Webpack 默认会将每个模块用函数包裹,Scope Hoisting 会分析模块依赖,尽量将所有模块合并到一个函数中,从而减少闭包数量、提升执行速度。

在 Webpack 4 中需引入 ModuleConcatenationPlugin,Webpack 5 生产模式下自动启用。

// Webpack 4 手动启用
plugins: [new webpack.optimize.ModuleConcatenationPlugin()];

前提条件是需要使用 ES Module 语法。

5. 使用长效缓存策略

要让浏览器缓存生效,需要保证文件内容变化时文件名也跟着变化(哈希),否则用户拿到的仍是旧文件。

  • 配置 output 文件名哈希

    output: {
      filename: '[name].[contenthash:8].js',
      chunkFilename: '[name].[contenthash:8].chunk.js',
    }
    

    contenthash 根据文件内容生成,只有内容改变时 hash 才变。

  • 提取稳定模板文件

    利用 HtmlWebpackPlugin 生成 HTML 并自动注入变化后的 script 标签。将不常变的 vendor 和 runtime 分离出来,保证业务代码变更时,三方库的缓存不被破坏。

    optimization: {
      runtimeChunk: 'single', // 单独提取 webpack 运行时
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /node_modules/,
            name: 'vendor',
            chunks: 'all',
          },
        },
      },
    }
    

三、开发体验优化

除了速度与体积,开发过程中的反馈速度同样重要。

1. 模块热替换(HMR)的正确姿势

HMR 允许在运行时更新模块,无需刷新页面,保持应用状态。

  • 启用 HMR 基础配置

    devServer: {
      hot: true,
      open: true,
    },
    // 并引入 webpack 内置插件(Webpack 5 自动包含)
    // plugins: [new webpack.HotModuleReplacementPlugin()]
    
  • 在代码中处理模块更新

    某些模块(如样式)会自动热更新,JS 逻辑则需要手动处理,避免状态丢失。

    if (module.hot) {
      module.hot.accept('./module', () => {
        // 重新执行渲染逻辑
      });
    }
    

2. 选择合适的 Devtool

开发时建议使用 eval-cheap-module-source-map,它在构建速度和源码映射精度之间取得了平衡。

3. 减少不必要的插件与 Loader

仅在生产环境执行压缩、代码分析等插件。利用 Webpack 的 mode 自动开启优化,避免开发模式下应用压缩导致速度变慢。

const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  return {
    optimization: {
      minimize: isProduction,
      minimizer: isProduction ? [new TerserPlugin()] : [],
    },
  };
};

4. 使用更快的 Loader

  • esbuild-loader 替代 babel-loader 可大幅提速(注意兼容性)。
  • CSS 处理链尽量精简,例如用 sass 替换 node-sass
rules: [
  {
    test: /\.js$/,
    loader: 'esbuild-loader',
    options: { target: 'es2015' },
  },
];

四、其它实用技巧

  • 分析打包结果
    借助 webpack-bundle-analyzer 可视化查看各模块体积,有针对性地优化。

    const BundleAnalyzerPlugin =
      require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    plugins: [new BundleAnalyzerPlugin()];
    
  • 懒编译(Lazy Compilation)
    Webpack 5 实验特性,动态导入的模块仅在被使用时才编译,极大加速大型项目开发启动速度。

    experiments: {
      lazyCompilation: true,
    }
    
  • external 外部化
    将体积大的库如 jQuery、React 从 CDN 引入,配置 externals,既减少打包体积又加快构建。

    externals: {
      react: 'React',
      'react-dom': 'ReactDOM',
    }
    
  • 使用 IgnorePlugin 忽略无用文件
    例如 moment.js 的语言包,按需引入:

    plugins: [new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ })];