文章目录
- 一、前言
- 二、你能收获
- 三、步入正题
- 1. 通过使用 webpack 自带插件(define-plugin) 来区分当前的环境。
- 2. 通过使用 webpack-merge 来进一步分离开发环境与生产环境的配置。
- 3. 通过使用 module 的 noParse 属性来指明无需解析的三方包。
- 4. 通过配置 module 下的 exclude 和 include 属性来指明 webpack 的 loader 处理区域。
- 5. 通过使用 webpack 的 ignore-plugin 插件来忽略导入的内容。
- 6. 通过使用 webpack 的 dllPlugin 插件来提升构建速度与优化 bundle 大小。
- 7. 通过使用 happypack 模块实现多线程打包。
- 8. webpack 自带优化 - tree shaking 和 scope hosting
- 9. 通过配置 optimization 的 splitChunks 实现公共代码抽离
- 10. 通过使用 import(x) 函数实现代码分割(Dynamic Import)
- 11. 通过配置 webpack 的 HRM 来提高开发效率
- 12. 通过使用 webpack-bundle-analyzer 插件来分析 bundle 的依赖模块
- 四、后语
- 五、参考
一、前言
- 本文是针对有 webpack 基础的猿们食用的,如果还不会用 webpack 去构建自己的项目请看博主的上一篇文章 【 还不会 webpack ? 一文带你用 webpack 打造溜溜的前端工具流 ! 】。
- webpack 确实极大程度上的方便了我们的开发,但是难免会有一些使用不当或者我们没有注意了解的地方阻碍了 webpack 的性能,所以我们需要了解一些优化 webpack 的手段来使得我们手中的利器重新焕发光彩。
二、你能收获
👺 12 种 webpack 优化手段。
🚀 感受到更快的性能体验。
🌈 使自己的 webpack 构建工具愈来愈趋向合理。
🎉 一步步带你更高效的优化自己的 webpack ,使得打包出来的产物更精简。
📦 利用 webpack 开箱即用,插件化的特点,来实现 webpack 的优化能力。
🗣 先快速使用 webpack 搭建一个工程,只需具备基本的功能即可。具体可查看 Git 仓库的 feature/20210425/FruitJ/init_project
分支或者直接点我,然后克隆下来一步步跟着笔者走下去。
三、步入正题
1. 通过使用 webpack 自带插件(define-plugin) 来区分当前的环境。
开发的时候难免有些代码会在开发环境和生产环境切换的时候会随之调节,对于这部分代码我们只能手动去更改,一处两处还好,如果是很多处。不仅这是一个机械活动很累很烦,也是一个风险极大的活动,万一哪个接口没有及时替换成线上的接口,就可能会引发一些问题。其中一种解决方案是我们在 webpack 注册一个 “全局变量” ,然后涉及到随着环境切换而变动的代码可以通过这个 webpack 注册的 “全局变量” 做一个开关。这样就可以很大程度避免上述问题。
- 修改 webpack.config.js ,使用 webpack 的 define-plugin 插件来注册全局变量。但是需要注意的是 webpack 会将我们这边注册的值去掉双引号,比如我们写
'DEV'
他是不会当成字符串 DEV 来处理的而是直接去掉最外层的引号变成了变量 DEV,所以为了解决这个问题通常都在单引号外面再套一层双引号("'DEV'"
),这样就可以正确表示'DEV'
啦,当然如果你想定义的值是个布尔类型的就不要再在外面套层双引号了直接这样写即可:'true'
。当然这种书写方式肯定不是大家想要的,因为需要多套一层引号,我们其实可以使用JSON.stringify(...)
来处理这种情况的(将"'DEV'"
变成JSON.stringify('DEV')
),在本小节中还是使用"'DEV'"
的方式,在下一个小节中我们将使用JSON.stringify('DEV')
方式。
...
plugins: [
...
new Webpack.DefinePlugin({
ENV: "'DEV'",
}),
],
...
webpack.config.js
const path = require("path");
const Webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new Webpack.DefinePlugin({
ENV: "'DEV'",
}),
],
module: {
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
- 重新编写 index.js 中的测试代码,直接使用刚刚注册的全局变量
ENV
来做判断即可。
index.js
import "./index.css";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
- 运行 yarn dev 命令查看效果,因为当前我们配置的值是
"'DEV'"
所以肯定会匹配 index.js 判断部分的if(ENV === 'DEV')
,所以肯定会打印访问开发环境 API【 http://localhost:3000/api/userInfo 】
。当我们将全局变量的值改为"'PROD'"
后就会匹配 index.js 判断部分的else if(ENV === 'PROD')
就会打印访问生产环境 API【 http://fruitj:3005/api/userInfo 】
。
小结 :
通过使用 webpack 中的 define-plugin 插件来注册全局变量进而区分具体的环境,目前我们已经达到了无需手动切换业务代码只需要切换开关就达到对应开发环境和生产环境的行为的变动。这样我们就不用担心因为来回切换环境再去切换业务代码的写法而导致的一些问题了。
2. 通过使用 webpack-merge 来进一步分离开发环境与生产环境的配置。
虽然在上一节中我们通过 webpack 的 define-plugin 插件注册全局环境变量已经实现了环境改变无需切换具体的业务代码就能展现对应的行为的能力了。但是这还是不够因为我们现在的 webpack.config.js 中的配置还是耦合了开发环境和生产环境的全部配置。一方面虽然可以这么做但是不规范,另一方面就是随着项目愈来愈复杂、配置愈来愈多如果都耦合在一起的话会非常难维护。所以我们需要将开发环境和生产环境的配置严格区分开来,但是对于共同配置还是需要合并进来的,这就需要 webpack-merge 来帮助我们在分离开发环境与生产环境的配置后将公共配置 merge 进来。
- 将
webpack.config.js
改名为webpack.base.js
,这个 base 配置文件就作为 webpack 的基本配置文件,就是这个文件是用来存放开发环境和生产环境公共配置。
webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
- 新建
webpack.dev.js
文件,这里面就可以使用webpack-merge
将 base 的配置 merge 到当前文件中,并且在此文件中配置一些开发环境用到的专有配置。
webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
],
});
- 新建
webpack.prod.js
文件,这里面就可以使用webpack-merge
将 base 的配置 merge 到当前文件中,并且在此文件中配置一些生产环境用到的专有配置。
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
},
});
- 好啦配置完毕,但是有个问题。就是 webpack 怎么知道该去读哪个配置呢 ? 通俗说就是 webpack 怎么知道我 yarn build 就去读 webpack.prod.js 的配置、我 yarn dev 就去读我 webpack.dev.js 的配置呢 ? 还记得在上一篇文章介绍的在 package.json 的 scripts 属性中配置命令吗 ? 这里就可以通过
--config
指定读取的具体配置文件。
...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 <wb-lj789114@alibaba-inc.com>",
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17"
}
}
这样我们执行 yarn build,webpack 就会去读取 webpack.prod.js 配置文件,随即进行打包。我们执行 yarn dev,webpack 就会去读取 webpack.dev.js 配置文件,随即启动静态服务打开浏览器渲染页面 …
运行 yarn build 命令,打包出来 dist 目录后我们运行 index.html 查看效果。
运行 yarn dev 命令,查看浏览器效果。
小结 :
这一节通过 webpack-merge 我们实现了开发环境和生产环境的分离,实现了 yarn build 和 yarn dev 分别走不同环境的配置文件。这样后期相对来说会比较好维护一些。
同时我们也可以不用手动切换我们在 webpack 全局注册的 ENV 变量了,因为在 webpack.dev.js 和 webpack.prod.js 都分别指定好了,当运行 yarn dev 和 yarn build 命令的时候执行的是对应的配置文件,在全局注册的 ENV 变量的值就是对应的值。
3. 通过使用 module 的 noParse 属性来指明无需解析的三方包。
我们的项目中可能包含有大量的类似 “jQuery” 这种不依赖或者说很少依赖其它模块的库,因为这些库本身就是一个最简最小的三方库了。而 webpack 在打包之前是先会以 output 规定的入口开始对每一个依赖资源进行解析的,解析这些依赖资源里面有没有再依赖其它资源。由此像 “jQuery” 这种包本身就不用去解析它了,它里面也不会依赖其它的资源。所以我们可以在 webpack 中配置 noParse 属性来告诉 webpack 不需要解析哪些模块。
- 安装 juqery
~$ yarn add jquery --save
- 在 webpack.base.js 中的 module 属性下配置 noParse 属性
...
module: {
noParse: /jquery/,
...
},
...
webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
小结 :
这一节我们通过配置 webpack.base.js 中 module 属性的 noParse 来指明 webpack 不需要解析依赖的模块。这样 webpack 解析到该模块的时候就不会进去检查其内部依赖的模块了,如果这种模块只有少数几个则优化的效果还是不太容易看出来,但是如果这种模块比较多的话,就比较容易看出来了。
4. 通过配置 module 下的 exclude 和 include 属性来指明 webpack 的 loader 处理区域。
比如我们在配置 babel 转换的时候,test 我们是这样写的 /\.js$/
这不仅匹配了我们 src 目录同时也会匹配 node_module 目录。但是 node_modules 里面都是三方模块基本不需要我们的 babel 去做什么事情,所以我们要通过配置 babel 转换规则中的 exclude 属性来排除掉 node_modules 目录,或者使用 include 属性来指明只处理 src 目录。
...
module: {
...
rules: [
...
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
...
webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
小结 :
这一节我们通过配置 babel 规则中的 exclude 属性和 include 属性来指明 webpack 的处理区域,这对提升 webpack 通过 babel 做语法转换的时候也是一个不小的性能提升(因为少了匹配并处理 node_modules 的环节😂)。
5. 通过使用 webpack 的 ignore-plugin 插件来忽略导入的内容。
有时候在使用三方包的时,三方包会导入一些其它的依赖资源,但是这些资源又不是必须被依赖的,或者说我们不需要这些依赖的包。这个时候我们就可以使用 webpack 自带的 ignore-plugin 插件来指定忽视三方包中的特定依赖模块,被忽视依赖模块将不会被打包。
举个🌰 : 在使用 moment.js 的时候,moment 会将所有的本地化内容和核心功能一起打包。通俗说我们使用 moment.js 的时候他会自动去加载各国的多语言,如果我们的项目是一个国际化的项目那么也只能这样用,如果我们的项目只是一个国内的项目不涉及多语言那么就没有必要去加载世界各国的多语言了,只需要加载本国语言便好,这样最终的打包体积会小很多。
- 安装 moment.js 。
~$ yarn add moment --save
- 修改 index.js,写一段示例代码
import moment from "moment";
...
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
index.js
import "./index.css";
import $ from "jquery";
import moment from "moment";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
console.log($);
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
-
运行 yarn dev 命令,查看打包的 bundle 体积大小与浏览器结果。
可以看出当前打包体积是1.39M
,体积比较大了,这就是因为 moment 包把所有多语言加载进去导致的结果。浏览器看到效果确实已经是我们设定好的中国的语言的时间。
所以为了优化我们需要忽略 moment 去自动加载多语言改为我们自己手动引入它的本国的多语言。 -
通过观察安装好的 moment 包的 package.json 找到 moment 包的入口文件。
-
打开 ./moment.js 找到我们设定多语言的
locale
方法再找到里面调用的getLocale
方法,发现 getLocale 方法里面调用了loadLocale
方法,在 loadLocale 方法中我们可以清楚的看到这样的导入多语言的语句...require('./locale' + name)
。
笔者大概数了下,这货约是导入了 135 个国家的多语言 … 下一步我们就是需要将这个./locale
目录忽略掉。 -
修改 webpack.base.js 通过 webpack 的 ignore-plugin 来忽略 moment 中导入的
./locale
目录
const webpack = require("webpack");
...
plugins: [
...
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
],
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
- 因为我们已经忽略 moment 导入自己的多语言文件了,所以我们如果想看到本国的多语言就必须自己手动导入我们本国的多语言文件。
import moment from "moment";
import "moment/locale/zh-cn";
...
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
index.js
import "./index.css";
import $ from "jquery";
import moment from "moment";
import "moment/locale/zh-cn";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
console.log($);
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
- 再次运行 yarn dev,查看打包的 bundle 体积大小与浏览器结果。
我们发现从一开始的1.39M
,优化到了871Kb
。这中间优化了约552Kb
。
小结 :
这一节我们通过 webpack 的自带插件 ignore-plugin 实现了,忽视指定的三方包导入的模块,实现了一定程度的优化,用 moment 举例我们优化了约 552Kb
。这对我们的项目最终产出的 bundle 体积也是一次极大优化,552Kb
这个数字不小了 …
6. 通过使用 webpack 的 dllPlugin 插件来提升构建速度与优化 bundle 大小。
说起优化,不仅是针对最终打包上线运行,同时也是针对开发者在开发过程中的项目构建。如果开发者在每次调试的时候构建速度能够快一些,这将会大大节约开发者的时间从而提升开发效率。
举个🌰:项目中依赖的一些变动不是特别频繁的模块,我们就可以通过 webpack 的 dll-plugin 插件将其打进 “动态链接库” 中,这样我们构建的时候对于这些不常变动的模块直接就去动态链接库中去查找这样就不会参与打包了,大大提升了项目的构建速度,同时最终产出的 bundle 也会减少部分体积。
原理就是,原来我们只有一个主构建过程,在这个主构建过程中凡是依赖的资源全部一股脑的都被打包。但是使用 webpack 的 dll-plugin 插件后我们除了主构建过程还拥有了一个 dll 构建过程,这个 dll 构建过程是先于主构建过程执行的。就是说我们会先通过 dll 构建过程先将那些不常变动的模块打包出来备用(这就是上面说的将不常变动的模块打进动态链接库),然后当我们进行主构建过程的时候让 webpack 先到动态链接库中去找,如果找到了就直接使用,没有找到再去打包。这样我们项目的构建速度会大大提升且最终产出的 bundle 由于不包含那部分被 dll 构建过程构建的不常变动的模块,所以体积比以前会小一些。但是这样虽然优化了 bundle 的体积,但并不会优化页面的访问速度,因为这种机制会先将 dll 构建出来的产物请求回来,然后才会去走 bundle。所以对于页面访问的速度并无明显的优化,但是对于项目的构建速度,优化就非常明显了。
- 我们以 react 和 react-dom 为例,因为项目一旦使用了这两个包不会轻易的进行升级。我们可以将二者打进动态链接库中来优化构建速度和 bundle 体积。
安装 :
~$ yarn add react react-dom --save
- 因为我们要使用 react 语法,所以 babel 在转译的时候要告诉它使用
@babel/preset-react
预设中插件集进行转换。
安装 :
~$ yarn add @babel/preset-react -D --save
- .babelrc 中将
@babel/preset-react
配置进去
.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
- 修改 index.js ,写一句 react 测试代码
index.js
import "./index.css";
import React from "react";
import { render } from "react-dom";
render(<h1>webpack optimization ...</h1>, document.querySelector("#app"));
然后运行 yarn dev 命令,查看打包的大小与浏览器的效果。
可以看到,bundle 的体积是 1.35M
,这已经很大了。浏览器效果正常。
接下来就要开始优化啦 👊 …
-
优化之前先讲一点前置知识,就是正常我们打包后我们最终在打包的结果中是无法获取到我们输出的内容的。我们先来做个试验。
-
在 src 目录下新建
test.js
文件,然后在里面输出一个普通值。
test/js
module.exports = "哈哈";
- 新建
webpack.dll.js
作为 dll 构建过程的 webpack 配置文件。现在是为了验证所以里面的配置比较简单
webpack.dll.js
const path = require("path");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
test: "./src/test.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new CleanWebpackPlugin(),
],
};
- 为了方便操作在 package.json 中注册一个命令
"dll_build": "webpack --config webpack.dll.js"
。
...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js",
"dll_build": "webpack --config webpack.dll.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 <wb-lj789114@alibaba-inc.com>",
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js",
"dll_build": "webpack --config webpack.dll.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17",
"jquery": "^3.6.0",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
- 运行命令 yarn dll_build 查看打包的结果,发现虽然函数中将我们输出的结果返回了但是并没有变量接收,就导致我们在外面拿不到我们里面输出的内容。
- 倘若我们手动在外面定一个变量来接手肯定是没有问题的。
虽然拿到了,但是我们总不至于手工去操作这个吧 … - 通过配置 ouput 的
library
属性实现自动创建变量并接收打包结果的返回值,这里我们将变量取名为_dll
。
...
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
...
webpack.dll.js
const path = require("path");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
test: "./src/test.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
plugins: [
new CleanWebpackPlugin(),
],
};
- 然后运行命令 yarn dll_build 查看打包的结果,发现我们指定的变量已经被自动加上并且已经接收了打包结果的返回值。
- 但我们最终要 dll 构建的是
react
和react-dom
而不是./src/test.js
并且我们要使用 webpack 的 dll-plugin 插件来生成动态链接库的映射,通俗说就是借助 webpack 的 dll-plugin 插件会通过 output 输出中的 library 指定的名字来构建一个资源映射,后面webpack 在找的时候就可以根据这个资源映射去找了(至于 dll-plugin 怎么与 output 匹配上的就是根据 dll-plugin 的 name 属性和 output 的 library 属性)。
...
const webpack = require("webpack");
...
entry: {
dll: ["react", "react-dom"],
},
...
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: "_dll",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
webpack.dll.js
const path = require("path");
const webpack = require("webpack");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
dll: ["react", "react-dom"],
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: "_dll",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
};
- 再次运行命令 yarn dll_build ,查看打包结果,发现
manifest.json
这个资源映射也成功的打包了出来。
- 但是现在有个问题,就是怎么告诉 webpack 先根据
manifest.json
去动态链接库中去找资源呢 ? 这就需要借助 webpack 的另一个插件dll-reference-plugin
了。而且这个插件是在我们主构建过程的webpack.base.js
中配置的,我们只需要通过manifest
这个属性指明我们的manifest.json
资源映射所在位置即可。
...
plugins: [
...
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
- 这样就大功告成了吗 ? 还没有,因为我们虽然已经将动态链接库打包出来了,但是并没有在页面去引用它,所以我们还需要在页面引用一下
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack 优化</title>
</head>
<body>
<div id="app"></div>
<script src="./dll.js"></script>
</body>
</html>
- 因为我们配置了
clean-webpack-plugin
插件,所以我们每次构建都会将上一次的结果清楚掉,这里我们先临时将我们从 dll 构建过程中打包出来的 2 个文件先放在根目录下备份一下。
- 然后执行主构建命令 yarn build,再把刚才备份的 2 个文件挪回去。
这个时候我们发现主构建过程的速度明显变快,且 bundle 打包出来的也仅有1.21Kb
,相比一开始优化之前的1.35M
,bundle 大小优化了约1381Kb
。
由于我们在有配置 html-webpack-plugin 插件的时候 yarn dev 命令不会走自己的 contentBase 指定的 dist 目录而是走自己打包进内存的,所以我们不能直接使用 yarn dev 查看效果,因为 yarn dev 打包出来的结果是缺少从 dll 构建过程打包出来的 2 个文件的,因为我们无法手动的将这 2 个文件放进内存中。
所以我们可以直接访问 dist 目录下 index.html 来查看效果。
我们可以清晰的看到,效果与原效果是一样的,并且确实也请求了我们的动态链接库。
小结 :
这一节我们通过配置使用 webpack 的 dll-plugin 插件和 dll-reference-plugin 插件借助动态链接库的机制实现了优化构建速度与 bundle 的体积。因为动态链接库我们一旦通过 dll 构建过程构建完毕后你的话只要不遇到模块升级或者变动基本上是不用管它了,这样我们主的构建过程会提速一些的。
但是还需介绍后面的内容这里我们就先将 index.html 和 webpack.base.js 的相关代码注释掉啦。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack 优化</title>
</head>
<body>
<div id="app"></div>
<!-- <script src="./dll.js"></script> -->
</body>
</html>
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
7. 通过使用 happypack 模块实现多线程打包。
简单介绍下多线程的意思,就是一个线程占用 cpu,达到时间片后会主动让出 cpu 使用权,众多线程们再次去争夺时间片,这样循环往复,直到大家都把活干完。
当然如果项目工程较小的话,使用这个多线程效率不一定会上升,只有当项目工程比较大的时候这个多线程打包的效果才会变得明显。
这里我们以多线程打包 js 为例 :
- 安装 happypack
~$ yarn add happypack -D --save
- 在 webpack.base.js 中配置和使用 hapypack。首先我们原来处理 js 的 loader 是 babel-loader,这次我们使用 happypack 下的 loader,并且指定我们处理的是 js。然后再以插件的方式配置一下,指明它的 use 属性为我们的 babel-loader。
...
plugins: [
...
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
...
rules: [
...
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const Happypack = require("happypack");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
};
- 运行命令 yarn build,查看打包情况,发现 happypack 启用了 3 个线程进行打包,但是由于我们的测试项目工程太小所以效果不是特别明显。
小结 :
这一节我们通过配置使用 happypack 实现了多线程打包,但是如果是小的项目就不建议使用这种方式进行优化了,如果是项目比较大的还是比较推荐使用的。
8. webpack 自带优化 - tree shaking 和 scope hosting
- tree shaking 意思就是树摇动,树摇动就会将没用(老叶子或坏叶子)的叶子摇掉,留下新叶子。这个革故鼎新的过程就是 tree shaking,通俗说就是 webpack 会自动将没有使用到的代码去除,这部分代码不会参与最终的打包。
但是需要注意的是 tree shaking 只发生在生产环境打包中,开发环境打包是不会进行 tree shaking 的。
举个 🌰:我们在 test.js 中新建 2 个函数(eat、sleep)并将其导出,在 index.js 中我们只用到 eat,sleep 不用。看下开发环境和生产环境下 webpack 所做的 tree shaking 优化。
test.js
const eat = () => {
console.log("吃");
};
const sleep = () => {
console.log("睡");
};
export default {
eat,
sleep,
};
index.js
import action from './test';
action.eat();
运行命令 yarn build 查看打包的结果,发现 webpack 只是将 eat 方法的内容导了出来,并没有看见 sleep 方法。
运行命令 yarn dev,发现 eat 和 sleep 方法全部被打进 bundle 了。
由此可见在生产环境打包的时候 webpack 会帮助我们对代码进行 tree shaking。
- scope hosting 意为作用域提升,这是什么意思呢 ? 就是刚才我们也发现了明明我们是在 index.js 中导入的 test.js,但是在打包出来的 bundle 中并没有发现什么导入语句,而是 webpack 把他们全部提升到这里来处理了。
小结 :
通过这一节我们了解到了 webpack 自带的一些优化,比如 tree shaking 和 scope hosting,其实还有很多其它的自带优化,这里只介绍这两个。比如 webpack 会对那种无意义代码进行简化等 … 。
9. 通过配置 optimization 的 splitChunks 实现公共代码抽离
当我们的项目是多入口的时候,不同入口引入了相同文件,webpack 默认会将这个文件分别导进各自引它的入口中。这样会增加最终打包出来的 bundle 体积。所以我们需要将这个公共的部分代码给单独打包出来,然后各个入口直接引入就好了,这样会减少 bundle 体积。
优化之前先看下 webpack 在多入口情况下默认的导入机制
- 修改 test.js
test.js
console.log("呵呵");
- 新建
other.js
。
other.js
import "./test";
console.log("other");
- 修改 index.js
index.js
import "./test";
console.log("index");
- webpack.base.js 配置多入口,入口分别是【src/index.js、src/other.js】
...
entry: {
index: "./src/index.js",
other: "./src/other.js",
},
output: {
filename: "js/[name].[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const Happypack = require("happypack");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// entry: "./src/index.js",
entry: {
index: "./src/index.js",
other: "./src/other.js",
},
output: {
filename: "js/[name].[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
};
- 运行命令 yarn build,发现 test.js 的内容被分别打包进 index.js 和 other.js 中去了
开始优化 - 修改 webpack.prod.js ,在 optimization 下配置 splitChunks ,splitChunks 有几个常用属性值得说一下 :
- chunks : 指明哪类资源应用当前的优化策略
- initial(代表对于直接引入的模块采取该优化策略)
- async(代表对按需引入的模块采取该优化策略)
- all(不管是直接引入还是按需引入都采取该优化策略)
- minSize : 公共部分超过多少字节就采取该优化策略
- minChunks : 公共部分被引用多少次就采取该优化策略
- chunks : 指明哪类资源应用当前的优化策略
...
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
},
},
},
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
},
},
},
});
- 运行命令 yarn build,查看打包结果。
可以看到打包出来一个common~index~other.de1df520.js
。这个里面存放的就是index.js
和test.js
两个入口引入的公共代码,并且将其挂载到了window.webpackJsonp
上了,而打包出来的 index.js 和 test.js 也是直接用的 window.webpackJsonp 上挂载的公共代码。
- 上面我们完成了抽离普通的公共模块,但是有些时候我们需要抽离的是其它的三方模块,类似 jQuery 等,所以我们可以通过 splitChunks 的
vendor
属性来进行配置。
...
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
vendor: {
test: /node_modules/, // 指定匹配的目录
chunks: "all",
minSize: 0,
minChunks: 2,
},
},
},
},
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
vendor: {
test: /node_modules/, // 指定匹配的目录
chunks: "all",
minSize: 0,
minChunks: 2,
},
},
},
},
});
- 修改 index.js
import $ from "jquery";
console.log($, "index");
- 修改 other.js
import $ from "jquery";
console.log($, "other");
- 运行命令 yarn build,查看打包效果,发现和上面的效果一致,就是 jquery 被单独打包然后挂载到了
window.webpackJsonp
上,然后打包的 index.js 和 test.js 引用的 jquery 都是从 window.webpackJsonp 上拿的。
再点开 index.html 查看效果,发现 jQuery 函数正常打印。
小结 :
这一节我们了解到了,通过配置 webpack 的 optimization 中的 splitChunks 可以实现公共代码抽离,目前我们实现了抽离普通的公共代码和三方库的代码。相当于将多个入口中公共的代码抽离了出来,会在一定程度上减少最终打包的 bundle 的体积。
10. 通过使用 import(x) 函数实现代码分割(Dynamic Import)
我们都知道凡是我们使用 import 语法导入的资源 webpack 都会在开始的时候全部打包进去,但是比如有个需求,我点击按钮出现一个弹框,弹框用到了 moment 要显示时间,对于完成这个业务需求来说可谓比较简单。但是我们在完成需求之后还可以进一步优化的,比如这个弹框是在我点击按钮之后才会使用的,那么为啥我不在它点击的时候去请求资源呢(moment) ? 在 webpack 中我们可以借助 import( ... )
来实现这个功能,这样就实现了拆分主 bundle,也就是我们常说的分包。
下面我们写个案例对比一下 : 案例的内容就是点击按钮打印 jquery。
未进行代码分割
- 修改 index.js
index.js
import $ from "jquery";
const onClickDynamicImport = () => {
console.log($);
};
const btn = document.createElement("button");
btn.innerText = "Dynamic Import";
btn.addEventListener('click', onClickDynamicImport, false);
document.querySelector("#app").appendChild(btn);
- 运行命令 yarn build 查看打包结果,发现 jQuery 被打进了主 bundle 中。
- 打开 dist 目录下的 index.html,点击按钮发现正常打印出了 jQuery 函数。
进行代码分割
- 改写 index.js 中获取 jQuery 的方式,采用 import(x) 这种动态导入的方式。
其中/* webpackChunkName: "jquery-chunk" */
这个写在 import( … ) 括号里面的注释是有用的不能删掉,比如这个webpackChunkName
属性就是为当前的 dynamic import 的模块起个名字的意思。还有一些其它的注释
index.js
const onClickDynamicImport = () => {
const result = import(/* webpackChunkName: "jquery-chunk" */ "jquery");
result.then(module => module.default).then($ => {
console.log($);
}).catch(error => {
console.log(error);
})
};
const btn = document.createElement("button");
btn.innerText = "Dynamic Import";
btn.addEventListener('click', onClickDynamicImport, false);
document.querySelector("#app").appendChild(btn);
- 运行命令 yarn build ,查看打包结果发现 jQuery 并没有被打进主 bundle 中,并且将 jQuery 单独打包出了一个文件
vendors~jquery-chunk.e0954a3f.js
当页面按钮被点击时就会去请求这个文件。
而且我们与未进行代码分割之前做下对比,分割前的主 bundle 大小是88.4Kb
。而打包后的大小是2.31Kb
,优化了约86Kb
左右。这个代码分割产生的优化效果是直接可以作用到页面的首屏访问速度上去的,因为被分割的代码不会在页面一开始渲染的时候一起请求过来,而是等需要的时候才会请求过来,所以对于项目中一起不常用且非必须在首屏就要加载的模块我们最好使用代码分割将他们分割出去,来提升页面的访问速度。 - 打开 dist 目录下的 index.html 查看效果。发现我们代码分割成的 jQuery 文件(
vendors~jquery-chunk.e0954a3f.js
)是点击按钮之后才请求过来的。
test
小结 :
这一节我们借助 webpack 的能力使用 ES6的 import 函数(Dynamic Import) 实现了代码分割,这种代码分割是一种比较实用的优化手段的,因为优化的结果可以直接作用于页面的访问速度上。
11. 通过配置 webpack 的 HRM 来提高开发效率
HRM (Hot Module Replacement) 就是 热更新/热替换 的意思,通俗说就是在开发阶段,代码改变浏览器无需重载页面,代码变动的地方页面会自动发生相应变化。试想一下如果页面的内容比较多,涉及到的逻辑也比较复杂,需要拉到许多资源,这个时候我们在开发阶段如果可以使用热更新功能,这会节省一部分的开发时间,提升我们的开发效率。因为不用重载整个网页了呀 ~ 。
配置热更新之前
- 修改 index.js
index.js
console.log("index");
- 运行命令 yarn dev,更改代码查看效果,发现确实代码改变的时候浏览器会自动重载页面。
test
配置热更新
- 修改 webpack.dev.js ,devServer 中使用 hot 属性并置为 true,并使用 webpack 的
HotModuleReplacementPlugin
和NamedModulesPlugin
插件,HotModuleReplacementPlugin 的作用是提供热更新支持,而 NamedModulesPlugin 插件的作用是打印更新模块的路径。
webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
hot: true,
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
new Webpack.NamedModulesPlugin(),
new Webpack.HotModuleReplacementPlugin(),
],
});
- 修改 index.js 在业务代码的最上端加上一段热更新的代码。
index.js
if(module.hot) module.hot.accept();
console.log("index");
- 运行命令 yarn dev,查看效果,发现无论怎么更改 index.js 中的代码,浏览器都不会重新重载,而是在原有的基础上进行 热更新/热替换。
test
- 其实我们也可以不在 webpack.dev.js 中明确使用 webpack 的
HotModuleReplacementPlugin
和NamedModulesPlugin
插件,我们可以配置一下 package.json 中的 yarn dev 命令,在其后添加一个--hot
参数,添加这个参数 webpack 就会帮助我们启用HotModuleReplacementPlugin
和NamedModulesPlugin
插件。 - 注释 webpack.dev.js 中的
HotModuleReplacementPlugin
和NamedModulesPlugin
插件。
webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
hot: true,
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
// new Webpack.NamedModulesPlugin(),
// new Webpack.HotModuleReplacementPlugin(),
],
});
- package.json 的 yarn dev命令添加
--hot
参数
...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js --hot",
"dll_build": "webpack --config webpack.dll.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 <wb-lj789114@alibaba-inc.com>",
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js --hot",
"dll_build": "webpack --config webpack.dll.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"happypack": "^5.0.1",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17",
"jquery": "^3.6.0",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
- 运行命令 yarn dev,你会发现与上面使用那两个插件并无二致。这种方式相对来说简单一些 …
小结 :
这一节我们借助 webpack 的配置实现了 HRM。这对于开发较复杂需要加载很多资源的页面是很有帮助的,因为每次代码变更浏览器不用重载页面了,只需要热更新代码变动的部分,这样会提升一定的开发速度。
12. 通过使用 webpack-bundle-analyzer 插件来分析 bundle 的依赖模块
如果项目比较小,像我们这个测试的小项目其实是完全用不上这个插件的,因为使用的 三方包也不多,心里都知道最终会将哪个三方包打进 bundle 中,但是如果项目比较大,引的三方包比较多,或者自己写的模块比较多。这个时候就需要对最终产出的 bundle 进行分析了,但是我们直接看压缩过的源码那简直是为难人。所以可以借助 webpack-bundle-analyzer
插件生成的依赖图谱来进行分析。
- 安装
webpack-bundle-plugin
插件
~$ yarn add webpack-bundle-plugin -D --save
- 修改 webpack.prod.js,配置并使用该插件
...
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
...
plugins: [
...
new BundleAnalyzerPlugin(),
],
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
new BundleAnalyzerPlugin(),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
// splitChunks: {
// cacheGroups: {
// common: { // 处理非三方模块
// chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
// minSize: 0, // 公共部分超过 0 字节就执行
// minChunks: 2, // 公共部分被引用超过 2 次就执行
// },
// vendor: {
// test: /node_modules/, // 指定匹配的目录
// chunks: "all",
// minSize: 0,
// minChunks: 2,
// },
// },
// },
},
});
- 运行命令 yarn build,查看效果,发现这个插件会自动开启一个端口为
8888
的本地服务,并跳转导浏览器,此时就可以看到自己项目打包出来的 bundle 中的模块依赖图谱了。
复杂的项目图谱会复杂一些 …
例如 :
借助此插件我们可以非常方便的去分析我们的 bundle 应该怎么去优化,哪些需要进行代码分割、哪些模块是重复的只是版本不一样,我们只需将他们的版本升级成一致的就可以减少 bundle 体积、哪些模块是可以打进动态链接库的等等,对于开发分析 bundle ,减小 bundle 体积来说还是非常有帮助的。
小结 :
这一节通过配置使用 webpack-bundle-analyzer 插件实现了 bundle 模块依赖的可视化,极大便利我们分析 bundle 中的模块依赖。是优化 bundle 最直观的一件利器。
四、后语
本文是基于上一篇文章的基础上,介绍了 12 种 webpack 的优化手段。相信通过这些优化手段可以使得我们通过 webpack 构建的项目更加的健壮。
我们来回忆一下都介绍了哪些优化手段 ?
- webpack 自带插件(define-plugin)【 注册全局变量,避免因切换环境导致业务代码未及时变更而引发的一些问题 】
- webpack-merge 插件【 分离开发环境和生产环境配置后使用此插件将公共配置 merge 进来 】
- module 的 noParse 属性【 可以指明 webpack 不去解析哪个三方模块 】
- rules 下的 exclude 和 include【 指明此 loader 不处理哪部分和指明处理哪部分 】
- webpack 自带插件(ignore-plugin)【 指明忽略哪个模块中非必须的依赖模块 】
- webpack 自带插件(dll-plugin 和 dll-reference-plugin)【 通过配置动态链接库来优化构建速度,优化主 bundle 体积 】
- optimization 的 splitChunks【 多入口抽离公共代码 】
- import(x) 函数 (Dynamic Import)【 非首屏必须的模块可以使用代码分割来优化主 bundle 体积,优化的结果可直接作用于页面访问速度上 】
- happypack 插件【 实现多线程打包,对项目复杂的工程来说帮助大些 】
- webpack 自带优化【 tree-shaking 和 scope-hosting 】
- webpack HRM【 配置 webpack 热更新可以在浏览器无需重载页面的情况下更新代码的变更结果 】
- webpack-bundle-analyzer 插件【 通过此插件可以最直观的分析 bundle 中的模块依赖直至找出合适的优化手段 】
文章中虽然经过审阅但是难免会有错字、代码片段错误、贴图错误、甚至 npm 包不断升级可能跟到某处就卡了,如果出现这些问题请在博文下方留言,笔者发现第一时间进行处理。
本文是笔者一点点构建出来的绝对可以放心食用 🐮。