Webpack 生产模式与性能优化 —— 让你的应用加载快 80%
Webpack 生产模式与性能优化 —— 让你的应用加载快 80%
系列文章第 3 篇 | 预计阅读时间:25 分钟
一、生产模式核心目标
生产模式的目标只有一个:让用户更快地看到页面。
具体从三个维度优化:
- 减少体积 — 让用户下载更少的代码
- 提升性能 — 让代码运行更快
- 优化缓存 — 让二次访问更快
二、提取 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 启用条件
- ✅ 使用 ES Module 语法(
import/export) - ✅ 生产模式(Webpack 默认开启)
- ✅
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.js 和 admin.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)