删库在逃程序员的Blog

Webpack 高级优化实战 —— 生产级性能优化指南

author
·
15
0

Webpack 高级优化实战 —— 生产级性能优化指南

系列文章第 5 篇(最终篇) | 预计阅读时间:22 分钟


一、核心优化思路

生产级优化从三个维度入手:

  1. 构建速度优化 — 让打包更快
  2. 打包体积优化 — 让文件更小
  3. 运行性能优化 — 让代码更快

二、构建速度优化

2.1 多进程打包

适用场景: 大型项目(100+ 文件)

安装依赖:

npm i thread-loader -D

配置:

const os = require("os");
const threads = os.cpus().length;  // 获取 CPU 核心数

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: threads,  // 进程数量
            },
          },
          {
            loader: "babel-loader",
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      threads,  // ESLint 也开启多进程
    }),
  ],
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: threads,  // 压缩也开启多进程
      }),
    ],
  },
};

⚠️ 注意事项:

  • 进程启动开销约 600ms,小项目不建议使用
  • 进程数量 = CPU 核心数,不要设置过多

2.2 缓存优化

Babel 缓存:

{
  loader: "babel-loader",
  options: {
    cacheDirectory: true,      // 开启缓存
    cacheCompression: false,   // 不压缩缓存(更快)
  },
}

ESLint 缓存:

new ESLintWebpackPlugin({
  cache: true,
  cacheLocation: path.resolve(
    __dirname,
    "node_modules/.cache/.eslintcache"
  ),
});

2.3 缩小处理范围

使用 include 限定目录:

{
  test: /\.js$/,
  include: path.resolve(__dirname, "src"),  // 只处理 src 目录
  loader: "babel-loader",
}

使用 exclude 排除目录:

{
  test: /\.js$/,
  exclude: /node_modules/,  // 排除第三方代码
  loader: "babel-loader",
}

推荐: 使用 include 更精确,性能更好。


三、打包体积优化

3.1 Tree Shaking

原理: 移除未使用的 ES Module 代码

启用条件:

  1. ✅ 使用 ES Module 语法(import/export
  2. ✅ 生产模式(Webpack 默认开启)
  3. package.json 配置 "sideEffects"

配置:

{
  "sideEffects": false  // 所有文件无副作用
}

排除有副作用的文件(如 CSS):

{
  "sideEffects": [
    "*.css",
    "*.scss",
    "*.less"
  ]
}

3.2 Babel 优化

问题: Babel 为每个文件插入重复的辅助代码

解决方案: 使用 @babel/plugin-transform-runtime

安装依赖:

npm i @babel/plugin-transform-runtime @babel/runtime -D

babel.config.js 配置:

module.exports = {
  presets: ["@babel/preset-env"],
  plugins: [
    ["@babel/plugin-transform-runtime", {
      "corejs": 3,  // 启用 core-js polyfill
    }],
  ],
};

效果: 所有文件共享同一份辅助代码,体积减少 30%-50%。

3.3 代码分割

多入口分割

module.exports = {
  entry: {
    main: "./src/main.js",
    admin: "./src/admin.js",
  },
  output: {
    filename: "js/[name].js",
  },
};

提取公共代码

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        default: {
          minChunks: 2,        // 至少被引用 2 次
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

动态导入(按需加载)

// 点击按钮时才加载
document.getElementById("btn").onclick = async () => {
  const { sum } = await import("./math.js");
  console.log(sum(1, 2, 3, 4));
};

// 自定义 chunk 名称
import(/* webpackChunkName: "math" */ "./math.js")
  .then(({ sum }) => {
    console.log(sum(1, 2, 3, 4));
  });

输出: math.chunk.js


四、运行性能优化

4.1 Preload / Prefetch

Preload: 立即加载当前页面需要的资源

Prefetch: 浏览器空闲时加载下一个页面需要的资源

安装插件:

npm i @vue/preload-webpack-plugin -D

配置:

const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");

module.exports = {
  plugins: [
    new PreloadWebpackPlugin({
      rel: "preload",      // 或 prefetch
      as: "script",
    }),
  ],
};

HTML 输出:

<!-- Preload:立即加载 -->
<link rel="preload" href="js/math.chunk.js" as="script">

<!-- Prefetch:空闲时加载 -->
<link rel="prefetch" href="js/next-page.chunk.js">

4.2 网络缓存优化

Hash 类型对比:

Hash 类型 说明 适用场景
hash 整个项目的 hash ❌ 不推荐
chunkhash 根据入口生成 ⚠️ JS 文件
contenthash 根据内容生成 推荐

配置:

module.exports = {
  output: {
    filename: "js/[name].[contenthash:8].js",
    chunkFilename: "js/[name].[contenthash:8].chunk.js",
    assetModuleFilename: "media/[name].[contenthash:8][ext]",
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:8].css",
      chunkFilename: "css/[name].[contenthash:8].chunk.css",
    }),
  ],
};

4.3 Runtime 文件

问题: 修改一个模块,主文件 hash 也会变化

解决方案: 提取 runtime 到独立文件

module.exports = {
  optimization: {
    runtimeChunk: {
      name: "runtime",
    },
  },
};

效果: 修改业务代码时,只有业务文件和 runtime 变化。


五、PWA 离线缓存

5.1 什么是 PWA?

PWA(Progressive Web App) 是一种使用 Web 技术开发的应用,具有:

  • ✅ 离线访问能力
  • ✅ 可添加到主屏幕
  • ✅ 推送通知
  • ✅ 类似原生应用的体验

5.2 Workbox 配置

安装依赖:

npm i workbox-webpack-plugin -D

配置:

const WorkboxPlugin = require("workbox-webpack-plugin");

module.exports = {
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,    // 立即接管客户端
      skipWaiting: true,     // 跳过等待阶段
      runtimeCaching: [
        {
          urlPattern: /\.(png|jpe?g|gif|svg)$/,
          handler: "CacheFirst",
          options: {
            cacheName: "images-cache",
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 30 * 24 * 60 * 60,  // 30 天
            },
          },
        },
        {
          urlPattern: /\.js$/,
          handler: "StaleWhileRevalidate",
          options: {
            cacheName: "js-cache",
          },
        },
      ],
    }),
  ],
};

5.3 注册 Service Worker

// main.js
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/service-worker.js")
      .then((registration) => {
        console.log("SW registered:", registration);
      })
      .catch((error) => {
        console.log("SW registration failed:", error);
      });
  });
}

5.4 测试 PWA

使用 serve 启动本地服务器:

npm i serve -g
serve dist

注意: 直接打开 file:// 无法测试 Service Worker,必须使用 HTTP 服务器。


六、Core-js Polyfill

6.1 为什么需要 Polyfill?

问题: 新语法(如 Promise、Array.includes)在旧浏览器不支持

解决方案: 使用 core-js 注入 polyfill

6.2 配置方式

安装依赖:

npm i core-js -D

babel.config.js 配置:

module.exports = {
  presets: [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",      // 按需引入
      "corejs": 3,                 // core-js 版本
      "targets": {                 // 目标浏览器
        "chrome": "60",
        "firefox": "60",
        "ie": "11",
      },
    }],
  ],
};

效果: 自动检测代码中使用的 ES 新特性,按需注入 polyfill。

6.3 三种模式对比

模式 配置 特点
false 不注入 体积最小,兼容性差
entry 手动引入 core-js 体积较大
usage 自动检测 推荐,按需注入

七、性能分析工具

7.1 Webpack Bundle Analyzer

安装:

npm i webpack-bundle-analyzer -D

配置:

const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",  // 生成 HTML 报告
      openAnalyzer: false,     // 不自动打开
    }),
  ],
};

查看报告: 打开 dist/report.html

7.2 分析内容

  • 📊 每个模块的体积
  • 📊 依赖关系图
  • 📊 找出体积最大的包
  • 📊 优化建议

八、生产模式完整配置

8.1 最终配置

const os = require("os");
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");

const threads = os.cpus().length;

const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: ["postcss-preset-env"],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].[contenthash:8].js",
    chunkFilename: "js/[name].[contenthash:8].chunk.js",
    assetModuleFilename: "media/[name].[contenthash:8][ext]",
    clean: true,
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.css$/,
            use: getStyleLoaders(),
          },
          {
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
            test: /\.(png|jpe?g|gif|webp)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024,
              },
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
          },
          {
            test: /\.js$/,
            include: path.resolve(__dirname, "src"),
            use: [
              {
                loader: "thread-loader",
                options: { workers: threads },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                  plugins: ["@babel/plugin-transform-runtime"],
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "src"),
      exclude: /node_modules/,
      cache: true,
      threads,
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
      minify: {
        collapseWhitespace: true,
        removeComments: true,
      },
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:8].css",
      chunkFilename: "css/[name].[contenthash:8].chunk.css",
    }),
    new PreloadWebpackPlugin({
      rel: "preload",
      as: "script",
    }),
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
    }),
  ],
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin({
        parallel: threads,
      }),
    ],
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: "runtime",
    },
  },
  mode: "production",
  devtool: "source-map",
};

8.2 打包脚本

{
  "scripts": {
    "build": "npx webpack --config webpack.prod.js",
    "analyze": "npx webpack --config webpack.analyze.js"
  }
}

九、系列总结

9.1 五篇文章回顾

篇目 核心内容 关键技能
第 1 篇 入门与基础 Webpack 基本概念、快速上手
第 2 篇 开发模式 HMR、Source Map、OneOf、Cache
第 3 篇 生产模式 Tree Shaking、代码分割、网络缓存
第 4 篇 资源处理 CSS/JS/HTML/图片/字体处理
第 5 篇 高级优化 PWA、Core-js、性能分析

9.2 性能提升总结

优化项 效果
多进程打包 大型项目 2-3 倍
Tree Shaking 体积减少 20%-40%
代码分割 初始加载减少 50%+
网络缓存 二次加载快 80%+
Babel 优化 体积减少 30%-50%

9.3 最佳实践清单

  • ✅ 开发模式使用 cheap-module-source-map
  • ✅ 开启 HMR 热更新
  • ✅ 使用 oneOf 优化 loader 匹配
  • ✅ 用 include 限定处理范围
  • ✅ 开启 Babel 和 ESLint 缓存
  • ✅ 生产模式开启 Tree Shaking
  • ✅ 使用 contenthash 做长期缓存
  • ✅ 代码分割 + 动态导入
  • ✅ 大型项目开启多进程
  • ✅ 使用 Bundle Analyzer 分析体积

十、结语

🎉 恭喜!你已经完成了 Webpack 从入门到精通的完整学习!

接下来你可以:

  1. 📚 深入学习 Webpack 原理(Loader/Plugin 开发)
  2. 🛠️ 实战项目应用(React/Vue 项目配置)
  3. 🔧 自定义 Loader 和 Plugin
  4. 📖 阅读 Webpack 官方文档和源码

学习资源推荐:


系列导航:

  • 📌 第 1 篇:Webpack 入门与基础
  • 📌 第 2 篇:开发模式完全指南
  • 📌 第 3 篇:生产模式与性能优化
  • 📌 第 4 篇:资源处理大全
  • 📌 第 5 篇:高级优化实战(本文)

🎊 系列完结,感谢阅读!


本文基于 Webpack 5 编写,如有问题欢迎留言讨论。

评论 (0)