删库在逃程序员的Blog

Webpack 生产模式与性能优化 —— 让你的应用加载快 80%

author
·
7
0

Webpack 生产模式与性能优化 —— 让你的应用加载快 80%

系列文章第 3 篇 | 预计阅读时间:25 分钟


一、生产模式核心目标

生产模式的目标只有一个:让用户更快地看到页面

具体从三个维度优化:

  1. 减少体积 — 让用户下载更少的代码
  2. 提升性能 — 让代码运行更快
  3. 优化缓存 — 让二次访问更快

二、提取 CSS 为单独文件

2.1 为什么要提取 CSS?

开发模式: CSS 通过 style-loader 注入到 <style> 标签中

问题:

  • CSS 和 JS 混在一起,无法并行加载
  • 页面加载时会出现闪屏现象(先显示无样式 HTML,再渲染 CSS)
  • 无法利用浏览器 CSS 缓存

解决方案: 将 CSS 提取为独立的 .css 文件,通过 <link> 标签加载。

2.2 配置方法

安装插件:

npm i mini-css-extract-plugin -D

webpack.config.js 配置:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // 替换 style-loader
          "css-loader",
        ],
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "static/css/main.css",           // 主文件
      chunkFilename: "static/css/[name].css",    // 动态导入文件
    }),
  ],
};

效果: 生成独立的 main.css 文件,通过 <link> 标签加载。


三、CSS 压缩与兼容性处理

3.1 CSS 压缩

安装插件:

npm i css-minimizer-webpack-plugin -D

配置:

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),  // CSS 压缩
    ],
  },
};

压缩效果:

/* 压缩前 */
.box {
  width: 100px;
  height: 100px;
  background-color: pink;
}

/* 压缩后 */
.box{width:100px;height:100px;background-color:pink}

3.2 CSS 兼容性处理

使用 postcss-loader 自动添加浏览器前缀:

安装依赖:

npm i postcss-loader postcss postcss-preset-env -D

配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  "postcss-preset-env",  // 自动添加浏览器前缀
                ],
              },
            },
          },
        ],
      },
    ],
  },
};

效果: 自动添加 -webkit--moz- 等浏览器前缀。


四、Tree Shaking —— 摇掉无用代码

4.1 什么是 Tree Shaking?

Tree Shaking 是一个术语,用于描述移除 JavaScript 中没有使用的代码

原理: 依赖 ES Module 的静态结构特性,在编译时分析哪些导出没有被使用。

4.2 示例

工具库文件(utils.js):

export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  return a / b;
}

使用文件(main.js):

import { add } from "./utils";

console.log(add(1, 2));

打包结果: 只有 add 函数被打包,其他三个函数被摇掉。

4.3 启用条件

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

sideEffects 配置:

{
  "sideEffects": false  // 所有文件都没有副作用,可以安全 Tree Shaking
}

注意: 如果有副作用文件(如 CSS 文件),需要排除:

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

五、Babel 优化 —— 减少辅助代码

5.1 问题

Babel 编译时,会为每个文件插入辅助代码(如 _extends_classCallCheck):

// 编译前
class Person {
  constructor(name) {
    this.name = name;
  }
}

// 编译后(每个文件都插入)
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

问题: 如果有 100 个文件,辅助代码就重复 100 次,体积巨大。

5.2 解决方案

使用 @babel/plugin-transform-runtime 将辅助代码提取到独立模块:

安装依赖:

npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime -D  # 运行时依赖

babel.config.js 配置:

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

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


六、代码分割(Code Split)

6.1 为什么要代码分割?

问题: 所有代码打包成一个文件,体积过大(可能几 MB),用户首次加载慢。

解决方案: 将代码分割成多个小文件,按需加载

6.2 多入口分割

配置:

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

输出: 生成 main.jsadmin.js 两个文件。

6.3 提取公共代码

场景: 多入口文件引用了同一个模块,避免重复打包。

配置:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",  // 对所有模块进行分割
      cacheGroups: {
        default: {
          minChunks: 2,        // 至少被引用 2 次
          priority: -20,       // 权重
          reuseExistingChunk: true,  // 重用已有 chunk
        },
      },
    },
  },
};

效果: 公共模块提取到独立的 vendors.js 文件中。

6.4 动态导入(按需加载)

场景: 某些代码只在特定条件下使用(如点击按钮、路由切换)。

语法:

// 动态导入
document.getElementById("btn").onclick = async () => {
  const { sum } = await import("./math.js");
  console.log(sum(1, 2, 3, 4));
};

Webpack 配置:

module.exports = {
  output: {
    filename: "js/[name].js",
    chunkFilename: "js/[name].chunk.js",  // 动态导入文件命名
  },
  optimization: {
    splitChunks: {
      chunks: "all",
    },
  },
};

效果: math.js 被分割成独立的 chunk,点击按钮时才加载。

6.5 给动态导入文件命名

默认命名: 动态导入的文件名是数字(如 123.chunk.js),不利于缓存管理。

自定义命名:

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

输出: math.chunk.js


七、网络缓存优化

7.1 为什么需要缓存?

场景: 用户第一次访问网站后,第二次访问时:

  • 无缓存: 重新下载所有文件(慢)
  • 有缓存: 直接使用本地缓存(快)

问题: 如果文件名不变,浏览器会一直使用旧缓存,用户无法获取更新。

解决方案: 文件名带 hash 值,内容变化时 hash 变化,强制浏览器重新下载。

7.2 Hash 类型对比

Hash 类型 说明 适用场景
hash 整个项目的 hash,任意文件修改,所有文件 hash 都变 不推荐
chunkhash 根据入口文件生成,同入口的文件共享 hash JS 文件
contenthash 根据文件内容生成,内容不变 hash 不变 推荐

7.3 配置 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",
    }),
  ],
};

效果:

  • main.a1b2c3d4.js — 内容变化时 hash 变化
  • style.e5f6g7h8.css — CSS 内容变化时 hash 变化

7.4 Runtime 文件

问题: 修改一个模块,主文件的 hash 也会变化(因为引用关系变化)。

解决方案: 将 hash 映射关系提取到独立的 runtime.js 文件中。

配置:

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

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


八、Preload / Prefetch —— 预加载资源

8.1 为什么需要预加载?

场景: 用户点击按钮后才加载某个模块,如果模块体积大,会感到明显卡顿。

解决方案: 在浏览器空闲时提前加载资源。

8.2 Preload vs Prefetch

特性 Preload Prefetch
加载时机 立即加载 浏览器空闲时加载
优先级
适用范围 当前页面需要的资源 下一个页面需要的资源
兼容性 较好 较差

8.3 配置方法

安装插件:

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

webpack.config.js 配置:

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">

九、生产模式完整配置

9.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 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"),
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:8].css",
      chunkFilename: "css/[name].[contenthash:8].chunk.css",
    }),
    new PreloadWebpackPlugin({
      rel: "preload",
      as: "script",
    }),
  ],
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin({
        parallel: threads,
      }),
    ],
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: "runtime",
    },
  },
  mode: "production",
  devtool: "source-map",
};

9.2 打包脚本

package.json:

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

执行打包:

npm run build

十、小结

本篇我们学习了生产模式的完整优化方案:

优化项 作用 体积减少
提取 CSS 并行加载,避免闪屏 -
CSS 压缩 压缩 CSS 代码 30%-50%
Tree Shaking 移除未使用代码 20%-40%
Babel Runtime 减少辅助代码重复 30%-50%
代码分割 按需加载 -
contenthash 长期缓存 -
Preload/Prefetch 提前加载资源 感知速度提升

性能提升总结:

  • ✅ 首次加载体积减少 50%-70%
  • ✅ 二次加载利用缓存,速度提升 80%+
  • ✅ 按需加载,减少初始加载时间

下一篇预告: 《资源处理大全》—— CSS/JS/HTML/图片/字体等资源处理的完整指南,包含各种 Loader 的详细配置和最佳实践!


系列导航:

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

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

评论 (0)