webpack性能优化
在前一篇文章中,基础 webpack 环境已经搭建起来了,可以愉快地在上面写代码了。
但还不够,以上配置只是一个最基础的配置。我们还想要做一些优化方面的配置。优化有两个方面:
- 优化开发体验
- 优化输出质量
其中,为了优化开发体验,webpack 给我们提供了下面的方案:
- 使用 source-map 特性查看运行时的源代码
- 使用 WebpackDevServer 实现浏览器自动刷新
- 缩小文件范围,优化构建速度
- CSS 支持:支持 less、sass,并支持自动补齐浏览器前缀 postcss
- 优化文件监听范围
- DllPlugin 、HardSourceWebpackPlugin 优化构建性能
- 【重】使用 happypack 并发执行打包任务
为了优化输出质量,webpack 给我们提供了下面的方案:
- 抽离、压缩CSS
- 压缩 HTML
- 【重】Tree Shaking
- 代码分割 Code Splitting
接下来,就来看看对应的配置是怎样的吧。(文末会有对应配置的完整代码文件)

优化开发体验
source-map
在调试过程中,为了体验更好,我们需要使用 webpack 中的 devtool 这个特性。它有一个可设置的 souce-map 特性,包含了源代码与打包后代码的映射关系,可以通过 sourceMap 定位到源代码。
在 devtool 中,有如下几个选项:
- eval: 使用 eval 包裹模块代码
- source-map: 产生
.map文件 - cheap: 不包含列信息
- module: 囊括第三方模块,包含loader的 sourceMap
- inline: 将
.map作为DataURI嵌入,不单独生成.map文件
配置推荐:
//webpack.config.js
devtool: "cheap-module-eval-source-map"//开发环境配置
//线上不推荐开启
WebpackDevServer
每次改完代码都需要重新打包一次,打开浏览器,刷新一次,很麻烦。我们可以通过安装 webpackDevServer 来改善这方面的体验。
同时,可以通过代理 proxy 模式,将针对后端的请求代理到本地服务器或者测试服务器上,可以方便自测联调期间的体验。
-
安装
npm i webpack-dev-server -D -
配置
修改 package.json:
"scripts": { "server": "webpack-dev-server" }修改 webpack.config.js 配置:
devServer: { contentBase: "./dist", open: true, port: 8081, // webpack-dev-server 的本地端口 proxy: { // 通过代理,将以 /api 开头的请求,不再放入到 webpack-dev-server,而是 target 中 "/api": { target: "http://localhost:9092" } } } -
启动
npm run server
缩小文件范围
在开发期间,可以通过缩小文件范围来减少 webpack 不必要的文件扫描,加快每次打包的速度。
优化 resolve.modules 配置
resolve.modules ⽤于配置 webpack 去哪些目录下寻找第三⽅模块,默认是 ['node_modules']。如果没有找到,就会去上⼀级目录 ../node_modules 找,再没有会去 ../../node_modules 中找,以此类推。如果我们的第三方模块都安装在了项⽬根目录下,就可以直接指明这个路径。
优化 resolve.alias 配置
resolve.alias 配置通过别名来将原导⼊路路径映射成一个新的导⼊路径。
拿 react 为例,我们引入的react库,⼀般存在两套代码:
-
cjs:采⽤用commonJS规范的模块化代码
-
umd:已经打包好的完整代码,没有采用模块化,可以直接执⾏
默认情况下,webpack会从⼊口文件 ./node_modules/bin/react/index 开始递归解析和处理依赖的⽂文件。我们可以直接指定⽂件,避免这处的耗时。
优化resolve.extensions 配置
在导入语句没带文件后缀时,webpack会⾃动带上后缀后,去尝试查找文件是否存在。这里的 resolve.extensions 指的就是会带上的后缀。建议后缀尝试列表越少越好,我们这里只设置一个:
extensions:['.js']
于是,在 resolve 这一项中,得到的配置是这样的:
resolve: {
modules: [path.resolve(__dirname, "./node_modules")],
alias: {
"vue": path.resolve(
__dirname,
"./node_modules/vue/dist/vue.esm.js"
),
"react": path.resolve(
__dirname,
"./node_modules/react/umd/react.production.min.js"
),
"react-dom": path.resolve(
__dirname,
"./node_modules/react-dom/umd/react-dom.production.min.js"
)
},
extensions:['.js']
},
CSS 的支持
关于 CSS 的开发体验优化有两块:
- 支持使用 less 或 sass 作为 css 技术栈
- 使用 postcss 为样式自动补齐浏览器前缀。
这两块都是可以为开发过程省很多时间的。我习惯使用 less,于是特此安装对应的 loader。
安装
npm i less less-loader -D
npm i post-css-loader autoprefixer -D
使用
//webpack.config.js 中增加 less 类型对应的 loader
rules: [
{
test: /\.less$/,
use:["style-loader","css-loader","less-loader"]
}
]
//新建 postcss.config.js 配置 postcss中,需要增加的浏览器前缀
module.exports = {
plugins: [
require("autoprefixer")({
overrideBrowserslist: ["last 2 versions", ">1%"] //覆盖的浏览器类型
})
]
};
优化文件监听的性能
我们在调试的时候,期望开启文件监听模式,这样 webpack 就可以在发现文件有变动的时候,做对应的操作。上面的 devServer 就开启了文件监听。当然我们也可以自己开启,通过命令 wepack --watch即可实现。
一般情况下,我们是不需要监听第三方模块的,于是我们可以配置监听模式时候,不去监听第三方模块的文件夹。这样, webpack 消耗的内存和 CPU 都将会大大减少。
//webpack.config.js
module.export = {
watchOptions : {
ignored : '/node_modules/'
}
}
DllPlugin、HardSourceWebpackPlugin
还是针对第三方库模块。项目中引入了很多第三方库,这些库在很长的一段时间内,都是不会更新的。那么我们打包的时候可以分开打包来提升打包速度。只需要编译一次,编译完成后存在指定的文件(这里可以称为动态链接库)中。在之后的构建过程中不会再对这些模块进行编译,而是直接使用对应的插件来引用动态链接库的代码。因此可以大大提高构建速度。
本质上就是做缓存嘛!
这样的工作,之前是 DllPlugin 与 DllReferencePlugin 合力完成的。但是整个配置过程较为繁琐(读者若有经历过,可知。且 React Vue 都已经摒弃了 DLL 的方式)。现在有了一个新的插件叫:HardSourceWebpackPlugin。它可以完成与 DllPlugin 、 DllReferencePlugin 同样的工作,但是配置的过程极为简单:
//webpack.config.js
//引用
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
//使用
new HardSourceWebpackPlugin()

在第一次打包的时候,它的速度是相对而言较慢的,需要将对应的第三方库模块写入到 cache 中。但是之后的打包速度提升,效果是极为明显的!!
happypack 并发执行任务
运行在 Node 之上的 webpack 是单线程工作的,不能同时处理多个任务。 Happypack 就能够让 webpack 做到这一点:
happypack 通过new HappyPack(),去实例化一个 HappyPack 对象,其实就是告诉 Happypack 核心调度器如何通过一系列 loader 去转换一类文件,并且可以指定如何为这类转换器作分配子进程。 核心调度器的逻辑代码在主进程里,也就是运行 webpack 的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,子进程处理完后会将结果发送给核心调度器,它们之间的数据交换是通过进进程间的通讯 API 实现的。 核心调度器收到来自子进程处理完毕的结果后,会通知 webpack 该文件已经处理完毕。
- 配置 happyPack
//webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
//充分发挥多核的作用,进程数量设置为设备的核数
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
- 设置 loader 与 happyPack 对应关系,通过ID来对应。
module: {
rules: [
{
test: /\.css$/,
use: ["happypack/loader?id=css"]
},
{
test: /\.less$/,
use: ["happypack/loader?id=less"]
},
{
test:/\.(png|jpe?g|gif)$/,
use: ["happypack/loader?id=pic"]
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: ["happypack/loader?id=ttf"]
},
{
test: /\.js$/,
include: path.resolve(__dirname, "./src"),
use: ["happypack/loader?id=babel"]
}
]
},
- 设置 HappyPackPlugin 中更详细的 loader 配置信息:
plugins: [
new HappyPack({
id: "css",
loaders: ["style-loader", "css-loader"],
threadPool: happyThreadPool
}),
new HappyPack({
id: "less",
loaders: ["style-loader", "css-loader","less-loader"],
threadPool: happyThreadPool
}),
new HappyPack({
id: "pic",
loaders: [
{
loader: "file-loader",
options: {
name: "[name]_[hash:6].[ext]",
outputPath: "images/"
}
}
],
threadPool: happyThreadPool
}),
new HappyPack({
id: "ttf",
loaders: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
}
}
],
threadPool: happyThreadPool
}),
new HappyPack({
id: "babel",
loaders: [
{
loader: "babel-loader"
}
],
threadPool: happyThreadPool
}),
]

优化输出质量
上面的配置都是为了优化开发体验而做的配置。在开发完成之后,我们希望我们的文件输出质量也能够得到保障。简单讲,就是最终生成的文件大小越小越好。为了实现这个目标,webpack 给我们提供了一些解决方案:
抽离、压缩CSS
如果不做抽取配置,我们的 css 是直接打包进 JS 里面的,我们希望能够单独生成 css 文件。因为 单独生成的 css 文件可以和 js 文件并行下载(在网页加载的过程中),可以提高文件的加载效率。
webpack 给我们提供了 MiniCssExtractPlugin 来完成 CSS 的抽离。用法如下:
//安装
npm i mini-css-extract-plugin -D
//webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module: {
rules: [
{
test: /\.less$/,
include: path.resolve(__dirname, "./src"),
use: [
MiniCssExtractPlugin.loader, //此处需要特换 style-loader
"css-loader",
"postcss-loader",
"less-loader"
]
}
]
},
plugins : [
// 抽离css为独立文件输出
new MiniCssExtractPlugin({
filename: "css/[name]_[contenthash:6].css"
})
]
同时,还有插件可以让我们进行 css 文件的压缩:optimize-css-assets-webpack-plugin、cssnano。用法如下:
//安装
npm i cssnano optimize-css-assets-webpack-plugin -D
//webpack.config.js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
plugins: [
// 抽离css为独立文件输出
new MiniCssExtractPlugin({
filename: "css/[name]_[contenthash:6].css"
}),
new OptimizeCSSAssetsPlugin({
cssProcessor: require("cssnano"), //引入cssnano配置压缩选项
cssProcessorOptions: {
discardComments: { removeAll: true } // 将未用到的样式全部移除
}
}),
]
压缩 HTML
对于 HTML 的压缩,可以借助我们前一篇文章提到的 插件:htmlwebpackplugin。用法如下:
new htmlwebpackplugin({
title: "首页",
template: "./src/index.html",
filename: "index.html",
minify: {
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空白符与换行符
minifyCSS: true // 压缩内联css
}
}),
JavaScript Tree Shaking
关于 Tree Shaking 的知识点,可以参考百度外卖写的这篇文章。需要了解的一个关键一点是:
在 JavaScript 中,tree-shaking 的消除原理是依赖于 ES6 模块特性的。ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。所谓静态分析就是不执行代码,从字面量上对代码进行分析。
ES6 模块特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable的
在最新的 webpack 4 中,只要设置为生产模式,Tree Shaking 是默认开启的。
为了更好地达到 Tree Shaking 的目的,我们应该做到:
- 使用 ES6 模块语法(即
import和export)。 - 规范代码习惯,多使用纯函数,少写副作用代码。
- 如果有明确的副作用代码(比如
@babel/polyfill),须在项目package.json文件中,添加一个 "sideEffects" 入口。

CSS Tree Shaking
既然 JavaScript 代码中有 Tree Shaking,那么我们的 CSS 代码是不是也能够有一个这样的插件呢?当然有。
//安装
npm i -D purifycss-webpack purify-css glob-all
//webpack.config.js
const PurifyCSSPlugin = require("purifycss-webpack");
const glob = require("glob-all");
new PurifyCSSPlugin({
paths: glob.sync([// 要做 CSS Tree Shaking 的路径文件
path.resolve(__dirname, "./src/*.html"),
// 请注意,我们同样需要对 js 文件进行 tree shaking
// 因为 JS 能够执行 CSSOM 操作与 DOM 操作
path.resolve(__dirname, "./src/*.js")
])
})
代码分割 Code Splitting
打包完之后,所有页面只生成一个 bundle.js (已将 CSS 文件单独剥离)。此时,之所以要代码分割,考虑的是两个方面:
- 代码体积大,不利于下载。
- 没有合理利用浏览器下载资源( chrome中,默认最大同时可以有6个 HTTP 请求)
在 Webpack 4 之前,有一些插件是专门用来做代码分割的,如 SplitChunksPlugin、CommonsChunkPlugin。而现在,webpack 4将这个代码分割的工作内置了。对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy)。我们只需要在配置文件中书写即可。
//开启 optimization.splitChunks 时的 default 配置
optimization: {
splitChunks: {
chunks: 'async',//同步(initial)、异步(async)、所有模块有效(all)
minSize: 30000,//最小尺寸,当模块大于30KB
maxSize: 0,//对模块进行二次分割时候使用,不推荐使用
minChunks: 1,//打包生成的chunk文件最少有另外多少个chunk引用了它
maxAsyncRequests: 5,//最大异步请求数
maxInitialRequests: 3,//最大初始化请求数
automaticNameDelimiter: '~',// 打包分割符号
name: true, //打包后的名称
cacheGroups: { //缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
在我们这里,会这样使用这个代码分割配置:
optimization: {
usedExports: true,
splitChunks: {
chunks: "all",
automaticNameDelimiter: "-",
cacheGroups: {
//缓存组
lodash: {
test: /lodash/,
name: "lodash",
minChunks: 1
},
react: {
test: /react|react-dom/,
name: "react",
minChunks: 1
},
vue: {
test: /vue/,
name: "vue",
minChunks: 1
}
}
}
}
总结
因为上面的配置有开发模式与生产模式的区别,最终将其配置项做了一个区分与合并。分为三个文件:
//webpack.config.js
const merge = require("webpack-merge");
const baseConfig = require("./webpack.config.base.js");
const prodConfig = require("./webpack.config.prod.js");
const devConfig = require("./webpack.config.dev.js");
module.exports = env => {
if (env && env.production) {
return merge(baseConfig, prodConfig);
} else {
return merge(baseConfig, devConfig);
}
};
对应的 package.json 配置为:
"scripts": {
"dev": "webpack",
"build": "webpack --env.production",
"server": "webpack-dev-server"
},
两种模式下的效果图:

最终配置好的代码在这里可以看到,取需即可。
前端性能优化系列: