其他章节请看:

webpack 快速入门 系列

性能

本篇主要介绍 webpack 中的一些常用性能,包括热模块替换、source map、oneOf、缓存、tree shaking、代码分割、懒加载、渐进式网络应用程序、多进程打包、外部扩展(externals)和动态链接(dll)。

准备本篇的环境

虽然可以仅展示核心代码,但笔者认为在一个完整的环境中边看边做,举一反三,效果更佳。

这里的环境其实就是实战一一文完整的示例,包含打包样式、打包图片、以及打包javascript

项目结果如下:

webpack-example3
- src // 项目源码
- index.html // 页面模板
- index.js // 入口
- package.json // 存放了项目依赖的包
- webpack.config.js // webpack配置文件

代码如下:

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=`, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>请查看控制台</p>
<span class='m-box img-from-less'></span>
</body>
</html>
// index.js
console.log('hello');
// package.json
{
"name": "webpack-example3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.14.2",
"babel-loader": "^8.2.2",
"core-js": "3.11",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-webpack-plugin": "^2.5.4",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.2",
"less-loader": "^7.3.0",
"mini-css-extract-plugin": "^1.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.3.0",
"postcss-preset-env": "^6.7.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2"
}
}
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin'); process.env.NODE_ENV = 'development' const postcssLoader = {
loader: 'postcss-loader',
options: {
// postcss 只是个平台,具体功能需要使用插件
// Set PostCSS options and plugins
postcssOptions:{
plugins:[
// 配置插件 postcss-preset-env
[
"postcss-preset-env",
{
// browsers: 'chrome > 10',
// stage:
},
],
]
}
}
} module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/i,
// 将 style-loader 改为 MiniCssExtractPlugin.loader
use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],
},
{
test: /\.less$/i,
loader: [
// 将 style-loader 改为 MiniCssExtractPlugin.loader
MiniCssExtractPlugin.loader,
"css-loader",
postcssLoader,
"less-loader",
],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 指定文件的最大大小(以字节为单位)
limit: 1024*6,
},
},
],
},
// +
{
test: /\.html$/i,
loader: 'html-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
// +
{
// 配置处理polyfill的方式
useBuiltIns: "usage",
// 版本与我们下载的版本保持一致
corejs: { version: "3.11"},
"targets": "> 0.25%, not dead"
}
]
]
}
}
}
]
},
plugins: [
new MiniCssExtractPlugin(),
new OptimizeCssAssetsPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
// new ESLintPlugin({
// // 将启用ESLint自动修复功能。此选项将更改源文件
// fix: true
// })
],
mode: 'development',
devServer: {
open: true,
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
},
};

Tip: 由于本篇不需要 eslint,为避免影响,所以先注释。

在 webpack-example3 目录下运行项目:

// 安装项目依赖的包
> npm i
// 启动服务
> npm run dev

浏览器会自动打开页面,如果看到”请查看控制台“,控制台也输出了“hello”,说明环境准备就绪。

:笔者运行 npm i 时出现了一些问题,在公司运行 npm i 验证此文是否正确,结果下载得很慢(好似卡住了),于是改为淘宝镜像 cnpm i,这次仅花少许时间就执行完毕,接着运行 npm run dev 却在终端报错。于是根据错误提示安装 babel-loader@7 ,再次重启服务,问题仍旧没有解决。回家后,运行 npm i,依赖安装成功,可能环境也很重要。

// 终端报错
...
babel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.

热模块替换

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。

Tip: HMR 不适用于生产环境,这意味着它应当用于开发环境

下面我们就从 html、css 和 js 三个角度来体验热模块替换。

启用 hmr

此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置, 然后使用 webpack 内置的 HMR 插件。

配置 hot: true 就能启用 hmr。

// webpack.config.js
module.exports = {
devServer: {
// 开启热模块替换
hot: true
}
}

css 使用 hmr

新建一个 css 文件,通过 index.js 引入:

// a.css
p{color:blue;}
// index.js
import './a.css'

首先我们先不开启 hmr,重启服务(npm run dev),浏览器文字显示蓝色。如果改为红色(color:red;),你会发现整个页面都刷新了,文字变为红色。

接着开启hmr(hot: true),重启服务,再次修改颜色,文字的颜色会改变,但整个页面不会刷新。

Tip:如果觉得每次重启服务,都会自动打开浏览器页面,你可以注释掉 open: true 来关闭这个特征。

这里 css 热模块之所以生效,除了在 dev-server 中开启了 hmr,另一个是借助了 mini-css-extract-plugin 这个包;而借助 style-loader 使用模块热替换来加载 CSS 也这么简单。

html 使用 hmr

没有开启热模块替换之前,修改 index.html 中的文字,浏览器页面会自动刷新;而开启之后,修改 html 中的文字,浏览器页面就不会自动刷新。

将 index.html 也配置到入口(entry)中:

// webpack.config.js
module.exports = {
- entry: './src/index.js',
// 将 index.html 也作为入口文件
+ entry: ['./src/index.js', './src/index.html'],
}

重启服务,再次修改 index.html,浏览器页面自动刷新,热模块替换对 html 没生效。

// index.html

- <p>请查看控制台</p>
+ <p>请查看控制台2</p>

Tip:热模块替换,就是一个模块发生了变化,只变更这一个,其他模块无需变化;而 index.html 不像 index.js 会有多个模块,index.html 只有一个模块,就是它自己,所以也就不需要热模块替换。

js 使用 hmr

首先在 dev-server 中开启 hmr,然后创建一个 js 模块,接着在 index.js 中引入:

// a.js
const i = 1;
console.log(i);
// index.js
// 引入 a.js 模块
import './a';

此刻,你若修改 i 的值(const i = 2;),则会发现浏览器页面会刷新。

要让热模块替换在 js 中生效,我们需要修改代码:

// index.js

// 引入 a.js 模块
import './a'; if (module.hot) {
module.hot.accept('./a', () => {
console.log('Accepting the updated printMe module!');
});
}

再次修改 i 的值,控制台会输出新的值,但浏览器页面不会再刷新。

此时,如果你尝试给入口文件(index.js)底部增加一条语句 console.log('a');,你会发现浏览器还是会刷新。

所以这种方式对入口文件无效,只能处理非入口 js。

:如果一个 js 模块没有 HMR 处理函数,更新就会冒泡(bubble up)。

小结

模块热替换比较难以掌握。

社区还提供许多其他 loader,使 HMR 与各种框架和库平滑地进行交互:

  • Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
  • React Hot Loader: 实时调整 react 组件。

source map

source map,提供一种源代码到构建后代码的映射,如果构建后代码出错了,通过映射可以方便的找到源代码出错的地方。

初步体验

我们先故意弄一个语法错误,看浏览器的控制台如何提示:

// a.js
const i = 1;
// 下一行语法错误
console.log(i)();
// 控制台提示 a.js 第3行出错
Uncaught TypeError: console.log(...) is not a function a.js:3

点击“a.js:3”,显示内容为:

var i = 1; // 下一行语法错误

console.log(i)();

定位到了源码,很清晰。

假如换成 es6 的语法,点击进入的错误提示就没这么清晰了。请看示例:

// a.js
class Dog {
constructor(name) {
this.name = name;
} say() {
console.log(this.name)();
}
} new Dog('xiaole').say();
...
var Dog = /*#__PURE__*/function () {
function Dog(name) {
_classCallCheck(this, Dog); this.name = name;
} _createClass(Dog, [{
key: "say",
value: function say() {
console.log(this.name)(); // {1}
}
}]); return Dog;
}(); new Dog('xiaole').say();

错误提示会定位了行{1},我们看到的不在是自己编写的源码,而是通过 babel 编译后的代码。

接下来我们通过配置 devtool,选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

Tip:Devtool 控制是否生成,以及如何生成 source map。

// webpack.config.js
module.exports = {
devtool: 'source-map'
}

重启服务,通过错误提示点击进去,则会看到如下代码:

class Dog {
constructor(name) {
this.name = name;
} say() {
console.log(this.name)(); // {1}
}
} new Dog('xiaole').say();

不在是编译后的代码,而是我们的源码,而且在行{1}处,对错误也有清晰的提示。

不同的值

source map 格式有多种不同的值,以下是笔者对其中几种值的研究结论:

  • devtool: 'source-map'
> npm run build

1. 会生成一个 dist/main.js.map 文件
2. 在 dist/main.js 最后一行,有如下一行代码:
//# sourceMappingURL=main.js.map
3. 上文我们知道,调试能看到源码,官网文档的描述是 `quality 是 original`
4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
5. 官网推荐其可作为生产的选择
  • devtool: inline-source-map
> npm run build

1. 没生成一个 dist/main.js.map 文件
2. 在 dist/main.js 最后一行,有如下一行代码:
//# sourceMappingURL=data:application/json;charset=
3. 调试能看到源码
4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
  • devtool: eval-source-map
> npm run build

1. 没生成一个 dist/main.js.map 文件
2. 在 dist/main.js 中有 15 处 sourceMappingURL。而 inline-source-map 只有一处。
3. 调试能看到源码
4. 构建(build)速度最慢(slowest),但重建(rebuild)速度正常(ok)
5. 官网推荐其可作为开发的选择
  • devtool: hidden-source-map
> npm run build

1. 生成一个 dist/main.js.map 文件
2. 点击错误提示,看到的是编译后的代码
Uncaught TypeError: console.log(...) is not a function main.js:11508
3. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)

:官网说 hidden-source-map 的品质是 original,但笔者这里却是编译后的!

如何选择

source map 有很多不同的值,我们该如何选择?

幸好官网给出了建议。

开发环境,我们要求构建速度要快,方便调试:

  • eval-source-map,每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。

生成环境,考虑到代码是否要隐藏,是否需要方便调试:

  • source-map,整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。官网推荐其可作为生产的选择。
  • (none)(省略 devtool 选项),不生成 source map,也是一个不错的选择

Tip:若你还有一些特别的需求,就去官网寻找答案

oneOf

oneof 与下面程序的 break 作用类似:

let count = 1
for(; count < 10; count++){
if(count === 3){
break;
}
}
console.log(`匹配了${count}次`) // 匹配了3次

这段代码,只要 count 等于 3,就会被 break 中断退出循环。

通常,我们会这样定义多个规则:

module: {
rules: [{
test: /\.css$/i,
loader: ...
},
{
test: /\.css$/i,
loader: ...
},
{
test: /\.less$/i,
loader: ...
},
{
test: /\.(png|jpg|gif)$/i,
loader: ...
}
...
]

当 a.css 匹配了第一个规则,还会继续尝试匹配剩余的规则。而我希望提高一下性能,只要匹配上,就不在匹配剩余规则。则可以使用 Rule.oneOf,就像这样:

module: {
rules: [
{
oneOf: [{
test: /\.css$/i,
loader: ...
},
{
test: /\.less$/i,
loader: ...
},
{
test: /\.(png|jpg|gif)$/i,
loader: ...
}
...
]
}
]

如果同一种文件需要执行多个 loader,就像这里 css 有 2 个 loader。我们可以把其中一个 loader 提到 rules 中,就像这样:

module: {
rules: [
{
test: /\.css$/i,
// 优先执行
enforce: 'pre'
loader: ...
},
{
oneOf: [{
test: /\.css$/i,
loader: ...
},
...
]
}
]

Tip: 可以通过配置 enforce 指定优先执行该loader

缓存

babel 缓存

让第二次构建速度更快。

配置很简单,就是给 babel-loader 添加一个选项:

{
loader: 'babel-loader',
options: {
presets: [
...
],
// 开启缓存
cacheDirectory: true
}
}

Tip:因为要经过 babel-loader 编译,如果代码量太少,就不太准确,建议找大量的 es6 代码自行测试。

静态资源的缓存

Tip: 本小节讲的其实就是 hash、chunkhash和conenthash。

通常我们将代码编译到 dist 目录中,然后发布到服务器上,对于一些静态资源,我们会设置其缓存。

具体做法如下:

通过命令 npm run build 将代码编译到 dist 目录;

接着通过 express 启动服务,该服务会读取 dist 中的内容,相当于把代码发布到服务器上:

// 安装依赖
> npm i -D express@4
// 在项目根目录下创建一个服务:server.js
const express = require('express')
const app = express()
const port = 3001 app.use(express.static('dist')); // 监听服务
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
> nodemon server.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
Example app listening at http://localhost:3001

通过浏览器访问 http://localhost:3001,多刷新几次,在网络中会看见 main.js 的状态是 304,笔者这里的时间在2ms或5ms之间。

Tip:304 仍然会发送请求,通常请求头中 If-Modified-Since 的值和响应头中 Last-Modified 的值是相同的。

If-Modified-Since: Sat, 17 Jul 2021 02:34:06 GMT

Last-Modified: Sat, 17 Jul 2021 02:34:06 GMT

接下来我给静态资源增加缓存,这里就增加一个 10 秒的缓存:

// server.js

- app.use(express.static('dist'));
+ app.use(express.static('dist', { maxAge: 1000 * 10 }));

再次请求,发现 main.js 首先是 304,接下来10秒内状态码则是200,大小则指示来自内存,时间也变为 0 ms。过10秒后再次请求,又是 304。

现在有一个问题,在强缓存期间,如果出现了bug,我们哪怕修复了,用户使用却还是缓存中有问题的代码。

我们模拟一下这个过程图:先将缓存改长一点,比如 1 天,用户访问先输出 1,让浏览器缓存后,我们再修改代码让其输出 2,用户再次访问会输出什么?

// server.js
app.use(express.static('dist', { maxAge: '1d' }));
// index.js
console.log('1');

重新打包生成 dist,接着用户通过浏览器访问,控制台输出 1。

修改 js,重新打包生成 dist,再次访问,控制台还是输入 1。

// index.js
console.log('2');

:不要强刷,因为用户不知道强刷,也不会去管。

于是我们打算从文件名入手来解决此问题,我们依次来看看 hash、chunkhash和conenthash。

hash

核心代码如下:

// index.js
import './a.css'
console.log('1');
// a.css
p{color:red;}
// webpack.config.js

module.exports = {
output: {
filename: 'main.[hash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:10].css",
})
]
}

重新打包:

> npm run build

> webpack-example3@1.0.0 build
> webpack Hash: b2e057d598ca9092abd3
Version: webpack 4.46.0
Time: 4837ms
Built at: 2021-07-14 8:17:54 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.b2e057d598.css 12 bytes main [emitted] [immutable] main
main.b2e057d598.js 5.22 KiB main [emitted] [immutable] main
Entrypoint main = main.b2e057d598.css main.b2e057d598.js main.b2e057d598.js.map

主要看生成的 css 和 js 文件,名字中都带有相同的值 b2e057d598,取的是生成的 Hash 的前10位。index.html 中也会自动引入对应的文件名。

现在浏览器访问,文字是红色,控制台输出1。

接着模拟修复缺陷,将文字改为蓝色,再次打包。

p{color:blue;}
> npm run build

> webpack-example3@1.0.0 build
> webpack Hash: ed2cd907a36536276d20
Version: webpack 4.46.0
Time: 4771ms
Built at: 2021-07-14 8:29:14 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.ed2cd907a3.css 13 bytes main [emitted] [immutable] main
main.ed2cd907a3.js 5.22 KiB main [emitted] [immutable] main

浏览器访问,文字确实变为蓝色。但 js 和 css 都重新请求了,再看打包生成的文件,js 和 css 也都重新生成了新的文件名。这个会导致一个问题,只修改一个文件,其他的所有缓存都会失效。

Tip:这里修复的是 css,如果修复 js 也同样会导致所有缓存失效。

chunkhash

hash 会导致所有缓存失效,我们将其改为 chunkhash,还是存在相同的问题。请看示例:

将 hash 改为 chunkhash:

// webpack.config.js

module.exports = {
output: {
filename: 'main.[chunkhash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[chunkhash:10].css",
})
]
}

修改 css,然后重新打包,发现 js 和 css 文件也都重新生成了,虽然 chunkhash 与 hash 值不相同,但 main.js 和 main.css 中的 chunkhash 是一样的:

> npm run build

> webpack-example3@1.0.0 build
> webpack Hash: 8c1c035175aae3d36fea
Version: webpack 4.46.0
Time: 5000ms
Built at: 2021-07-14 9:16:46 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.619734f520.css 13 bytes main [emitted] [immutable] main
main.619734f520.js 5.22 KiB main [emitted] [immutable] main

Tip: 通过入口文件引入的模块都属于一个 chunk。这里 css 是通过入口文件(index.js)引入的,所以 main.js 和 main.css 的 chunkhash 值相同。

contenthash

contenthash 是根据文件内容来的,可以较好的解决以上问题。请看示例:

将 chunkhash 改为 contenthash,然后打包:

// webpack.config.js

module.exports = {
output: {
filename: 'main.[contenthash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash:10].css",
})
]
}
> npm run build

> webpack-example3@1.0.0 build
> webpack Hash: 12994324788654e2ffc4
Version: webpack 4.46.0
Time: 5115ms
Built at: 2021-07-14 9:26:59 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.21668176f0.css 12 bytes main [emitted] [immutable] main
main.8983191438.js 5.22 KiB main [emitted] [immutable] main

这次,js 和 css 的 hash 值不在相同。通过浏览器访问多次后,main.js 和 main.css 也都被强缓存。

修改css:

p{color:yellow;}

打包发现 js(main.8983191438.js) 没有变,只有 css 变了:

> npm run build

> webpack-example3@1.0.0 build
> webpack Hash: 1598c3794090ebc6964c
Version: webpack 4.46.0
Time: 4905ms
Built at: 2021-07-14 9:31:14 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.0241bb73c4.css 13 bytes main [emitted] [immutable] main
main.8983191438.js 5.22 KiB main [emitted] [immutable] main

再次通过浏览器访问,发现 css 请求了新的文件,而 js 还是来自缓存。

Tip: 是否要将 hash 清除?

注:此刻运行 npm run build 会报错,为了不影响下面的介绍,所以将 hash 去除,source map 也不需要,一并删除。

ERROR in chunk main [entry]
Cannot use [chunkhash] or [contenthash] for chunk in 'main.[contenthash:10].js' (use [hash] instead)

tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

使用树摇非常简单,只需要满足两个条件:

  • 使用 es6 模块化
  • 模式(mode)开启production

直接演示,请看:

a.js 中导出 a 和 b,但在index.js 中只使用了a:

// a.js
export let a = 'hello'
export let b = 'jack'
// index.js
import { a } from './a.js'
console.log(a);

首先在开发模式下测试,发现 a.js 中的”hello“和”jack“都打包进去了,请看示例:

module.exports = {
mode: 'development',
}
// dist/main.js
// a 和 b 都被打包进来,尽管 b 没有被用到 var a = 'hello';
var b = 'jack';

而在生成模式下,只有用到的 a 才被打包进去,请看示例:

module.exports = {
mode: 'production',
}
// dist/main.js
// 只找到 hello,没有找到 jack console.log("hello")

将文件标记为 side-effect-free(无副作用)

在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。

通过 package.json 的 "sideEffects" 属性,来实现这种方式。

{
"sideEffects": false
}

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。

Tip:"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

我们通过一个例子说明下:

在入口文件引入 css 文件:

// index.js
import './a.css'
import { a } from './a.js'
console.log(a);
// a.css
p{color:yellow;}
// webapck.config.js
mode: 'production'

打包会生成 css:

> npm run build

     Asset       Size  Chunks             Chunk Names
index.html 342 bytes [emitted]
main.css 13 bytes 0 [emitted] main
main.js 1.3 KiB 0 [emitted] main

在 package.json 添加 "sideEffects": false,标注所有代码都不包含副作用:

{
"sideEffects": false
}

再次打包,则不会生成 css:

> npm run build

     Asset       Size  Chunks             Chunk Names
index.html 303 bytes [emitted]
main.js 1.3 KiB 0 [emitted] main

:所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

// package.json
{
"sideEffects": [
"*.css",
"*.less"
]
}

代码分割

将一个文件分割成多个,加载速度可能会更快,而且分割成多个文件后,还可以实现按需加载。

optimization.splitChunks

对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy) —— SplitChunksPlugin。

开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

Tip: SplitChunksPlugin的默认配置如下:

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};

默认配置很多,如果我们不需要修改,则不用管它们,下面我们来体验一下 splitChunks.chunks:

Tip:splitChunks.chunks,表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。

> npm i lodash@4
// index.js
import _ from 'lodash'; console.log(_);

打包只生成一个 js:

> npm run build

     Asset       Size  Chunks             Chunk Names
index.html 303 bytes [emitted]
main.js 72.7 KiB 0 [emitted] main

配置splitChunks.chunks:

// webapck.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};

再次打包,这次生成两个 js,其中Chunk Names 是 vendors~main 对应的就是 loadsh:

> npm run build

     Asset       Size  Chunks             Chunk Names
1.main.js 71.5 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.9 KiB 0 [emitted] main

同一个 chunk 中,如果 index.js 和 a.js 都引入 loadash,会如何打包?请看示例:

// index.js
import {a} from './a.js'
import _ from 'lodash';
console.log(a)
console.log(_);
// a.js
export let a = 'hello'
export let b = 'jack'
> npm run build

     Asset       Size  Chunks             Chunk Names
1.main.js 71.5 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.92 KiB 0 [emitted] main

同样是两个 js,而且 loadash 应该是公用了,因为 main.js 较上次只增加了 0.02 kb。

动态导入

使用动态导入可以分离出 chunk。

请看示例:

上文我们知道,这段代码打包会生成两个 js,其中 main.js 包含了 a.js。

// index.js
import {a} from './a.js'
import _ from 'lodash';
console.log(a)
console.log(_);

将其中的 a.js 改为动态导入的方式:

// index.js

import _ from 'lodash';
// 动态导入
import(/* webpackChunkName: 'a' */'./a').then((aModule) => {
console.log(aModule.a);
});
console.log(_);

打包:

> npm run build

     Asset       Size  Chunks             Chunk Names
0.main.js 192 bytes 0 [emitted] a
2.main.js 94.6 KiB 2 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 2.75 KiB 1 [emitted] main

其中 a.js 被单独打包成一个js(从 Chunk Names 为 a 可以得知)

懒加载

懒加载就是用到的时候在加载。

请看示例:

我们在入口文件注册一个点击事件,只有点击时才加载 a.js。

// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a' */'./a').then((aModule) => {
console.log(aModule.a);
});
};
// a.js
console.log('moduleA');
export let a = 'hello'
export let b = 'jack'

启动服务,测试:

> npm run dev

第一次点击:moduleA hello

第二次点击:hello

只有第一次点击,才会请求 a.js 模块。

Tip:懒加载其实用到的就是上文介绍的动态导入

预获取

思路可能是这样:

  1. 首先使用普通模式
  2. 普通模式下,一次性加载太多,而 a.js 这个文件又有点大,于是就使用懒加载,需要使用的时候在加载 a.js
  3. 触发点击事件,懒加载 a.js,但 a.js 很大,需要等待好几秒中才触发,于是我想预获取来减少等待的时间

将懒加载改为预获取:

// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {
console.log(aModule.a);
});
};

刷新浏览器,发现 a.js 被加载了;触发点击事件,输出 moduleA hello,再次点击,输出 hello。

Tip:浏览器中有如下一段代码:

// 指示着浏览器在闲置时间预取 0.main.a3f7d94cb1.js
<link rel="prefetch" as="script" href="0.main.a3f7d94cb1.js">

预获取和懒加载的不同是,预获取会在空闲的时候先加载。

渐进式网络应用程序

渐进式网络应用程序(progressive web application - PWA),是一种可以提供类似于 native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。

我们首先通过一个包来启动服务:

> npm i -D http-server@0
// package.json
{
"scripts": {
"start": "http-server dist"
},
}
> npm run build

启动服务:

> npm run start

> webpack-example3@1.0.0 start
> http-server dist Starting up http-server, serving dist
Available on:
http://192.168.85.1:8080
http://192.168.75.1:8080
http://192.168.0.103:8080
http://127.0.0.1:8080
Hit CTRL-C to stop the server

:多个 url 与适配器有关:

> ipconfig

以太网适配器 VMware Network Adapter VMnet1:
IPv4 地址 . . . . . . . . . . . . : 192.168.85.1 以太网适配器 VMware Network Adapter VMnet8:
IPv4 地址 . . . . . . . . . . . . : 192.168.75.1 无线局域网适配器 WLAN:
IPv4 地址 . . . . . . . . . . . . : 192.168.0.103

通过浏览器访问 http://127.0.0.1:8080。如果我们将服务器关闭,再次刷新页面,则不能再访问。

接下来我们要做的事:通过离线技术让网页再服务器关闭时还能访问。

请看示例:

添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:

> npm i -D workbox-webpack-plugin@6
// webapck.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
};

完成这些设置,再次打包,看下会发生什么:

> npm run build

              Asset       Size  Chunks             Chunk Names
0.main.js 192 bytes 0 [emitted] a
2.main.js 94.6 KiB 2 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 2.75 KiB 1 [emitted] main
service-worker.js 1.11 KiB [emitted]
workbox-15dd0bab.js 13.6 KiB [emitted]

生成了两个额外的文件:service-worker.js 和 workbox-15dd0bab.js。service-worker.js 是 Service Worker 文件。

值得高兴的是,我们现在已经创建出一个 Service Worker。接下来我们注册 Service Worker。

// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {
console.log(aModule.a);
});
}; if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}

再次运行 npm run build 来构建包含注册代码版本的应用程序。然后用 npm start 启动服务。访问 http://127.0.0.1:8080/ 并查看 console 控制台。在那里你应该看到:

SW registered

Tip:如果没有看见 SW registered,可以尝试强刷

现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。

Tip:更过 pwa 可以参考 "mdn 渐进式应用程序";淘宝(taobao.com)以前有 pwa,现在却没有了。

多进程打包

通过多进程打包,用的好可以加快打包的速度,用得不好甚至会更慢。

这里使用一个名为 thread-loader 包来做多进程打包。每个 worker 是一个单独的 node.js 进程,开销约 600 毫秒,还有一个进程间通信的开销。

:仅将此加载器用于昂贵的操作!比如 babel

我们演示一下:

未使用多进程打包时间是 3122ms:

// index.js
import _ from 'lodash'
console.log(_);
> npm run build
Hash: a4868f457d7ce754335b
Version: webpack 4.46.0
Time: 3031ms

加入多线程:

> npm i -D thread-loader@3
// webpack.config.js -> module.exports -> module.rules
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'thread-loader',
{
loader: 'babel-loader',
...
}
]
}
> npm run build

Hash: a4868f457d7ce754335b
Version: webpack 4.46.0
Time: 3401ms

构建时间更长。

Tip: 可能是代码中需要 babel 的 js 代码太少,所以导致多线程效果不明显。

外部扩展(externals)

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。

externals

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

例如 jQuery 这个库来自 cdn,则不需要将 jQuery 打包。请看示例:

Tip: 为了测试看得更清晰,注释掉 pwa 和 splitChunks。

> npm i jquery@3
// index.js
import $ from 'jquery'; console.log($);

打包生成一个 js,其中包含了 jquery:

> npm run build

              Asset       Size  Chunks             Chunk Names
1.main.js 88 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.9 KiB 0 [emitted] main

由于开启了 splitChunks,这里 1.main.js 就是 jquery。

使用 external 将 jQuery 排除:

// webpack.config.js
module.exports = {
externals: {
// jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行
jquery: 'jQuery'
}
};

在 index.html 中手动引入 jquery:

// src/index.html

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.7.2/jquery.min.js"></script>

Tip: 我们使用 bootstrap cdn。

再次打包,则不在包含 jquery:

> npm run build

              Asset        Size  Chunks             Chunk Names
index.html 303 bytes [emitted]
main.js 1.35 KiB 0 [emitted] main

Tip:如果你在开发模式(mode: 'development')下打包,你会发现 main.js 中会有如下这段代码:

/***/ "jquery":
/*!*************************!*\
!*** external "jQuery" ***!
\*************************/
/*! no static exports found */
/***/ (function(module, exports) { eval("module.exports = jQuery;\n\n//# sourceURL=webpack:///external_%22jQuery%22?"); /***/ })

这里的 jQuery 来自我们手动通过 <script src=> 引入 jquery 所产生的全局变量。

动态链接(dll)

所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。

对于 webpack 就是事先将常用又构建时间长的代码提前打包好,取名为 dll,后面打包时则直接使用 dll,用来提高打包速度

vue-cli 删除了 dll

在 vue-cli 提交记录中发现:remove DLL option。

原因是:dll 选项将被删除。 Webpack 4 应该提供足够好的性能,并且在 Vue CLI 中维护 DLL 模式的成本不再合理。

Tip: 详情请看issue

核心代码

附上项目最终核心文件,方便学习和解惑。

webapck.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin'); process.env.NODE_ENV = 'development' const postcssLoader = {
loader: 'postcss-loader',
options: {
// postcss 只是个平台,具体功能需要使用插件
// Set PostCSS options and plugins
postcssOptions: {
plugins: [
// 配置插件 postcss-preset-env
[
"postcss-preset-env",
{
// browsers: 'chrome > 10',
// stage:
},
],
]
}
}
} module.exports = {
entry: './src/index.js',
entry: ['./src/index.js', './src/index.html'],
output: {
filename: 'main.js',
// filename: 'main.[contenthash:10].js', path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/i,
// 将 style-loader 改为 MiniCssExtractPlugin.loader
use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],
},
{
test: /\.less$/i,
loader: [
// 将 style-loader 改为 MiniCssExtractPlugin.loader
MiniCssExtractPlugin.loader,
"css-loader",
postcssLoader,
"less-loader",
],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 指定文件的最大大小(以字节为单位)
limit: 1024 * 6,
},
},
],
},
// +
{
test: /\.html$/i,
loader: 'html-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 'thread-loader',
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
// +
{
// 配置处理polyfill的方式
useBuiltIns: "usage",
// 版本与我们下载的版本保持一致
corejs: { version: "3.11" },
"targets": "> 0.25%, not dead"
}
]
],
// 开启缓存
cacheDirectory: true
}
}]
}
]
},
plugins: [
// new MiniCssExtractPlugin(),
new MiniCssExtractPlugin({
// filename: "[name].[contenthash:10].css",
}),
new OptimizeCssAssetsPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
// new ESLintPlugin({
// // 将启用ESLint自动修复功能。此选项将更改源文件
// fix: true
// }),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
mode: 'development',
// mode: 'production',
devServer: {
// open: true,
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
},
devServer: {
// 开启热模块替换
hot: true
},
// devtool: 'eval-source-map',
optimization: {
splitChunks: {
chunks: 'all',
},
},
externals: {
// jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行
jquery: 'jQuery'
}
};

package.json

{
"name": "webpack-example3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server",
"start": "http-server dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.14.2",
"babel-loader": "^8.2.2",
"core-js": "3.11",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-webpack-plugin": "^2.5.4",
"express": "^4.17.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.2",
"http-server": "^0.12.3",
"less-loader": "^7.3.0",
"mini-css-extract-plugin": "^1.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.3.0",
"postcss-preset-env": "^6.7.0",
"thread-loader": "^3.0.4",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2",
"workbox-webpack-plugin": "^6.1.5"
},
"dependencies": {
"jquery": "^3.6.0",
"lodash": "^4.17.21",
"vue": "^2.6.14"
},
"sideEffects": false
}

其他章节请看:

webpack 快速入门 系列

webpack 快速入门 系列 —— 性能的更多相关文章

  1. webpack 快速入门 系列 - 自定义 wepack 上

    其他章节请看: webpack 快速入门 系列 自定义 wepack 上 通过"初步认识webpack"和"实战一"这 2 篇文章,我们已经学习了 webpac ...

  2. webpack 快速入门 系列 —— 初步认识 webpack

    初步认识 webpack webpack 是一种构建工具 webpack 是构建工具中的一种. 所谓构建,就是将资源转成浏览器可以识别的.比如我们用 less.es6 写代码,浏览器不能识别 less ...

  3. webpack 快速入门 系列 —— 实战一

    实战一 准备本篇的环境 虽然可以仅展示核心代码,但笔者认为在一个完整的环境中边看边做,举一反三,效果更佳. 这里的环境其实就是初步认识 webpack一文完整的示例,包含 webpack.devSer ...

  4. vue 快速入门 系列 —— vue loader 下

    其他章节请看: vue 快速入门 系列 vue loader 下 CSS Modules CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统.vue-loader 提供了与 CSS ...

  5. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  6. vue 快速入门 系列 —— vue-cli 下

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  7. 快速入门系列--WebAPI--01基础

    ASP.NET MVC和WebAPI已经是.NET Web部分的主流,刚开始时两个公用同一个管道,之后为了更加的轻量化(WebAPI是对WCF Restful的轻量化),WebAPI使用了新的管道,因 ...

  8. 快速入门系列--MVC--07与HTML5移动开发的结合

    现在移动互联网的盛行,跨平台并兼容不同设备的HTML5越来越盛行,很多公司都在将自己过去的非HTML5网站应用渐进式的转化为HTML5应用,使得一套代码可以兼容不同的物理终端设备和浏览器,极大的提高了 ...

  9. WPF快速入门系列(4)——深入解析WPF绑定

    一.引言 WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能.WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信 ...

随机推荐

  1. ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件

    虽然ASP.NET Core应用的路由是通过RouterMiddleware这个中间件来完成的,但是具体的路由解析功能都落在指定的Router对象上,不过我们依然有必要以代码实现的角度来介绍一下这个中 ...

  2. swiper.animate~之~可以执行两种动画的升级版的Swiper Animate

        1.下载插件swiper.animate-twice.min.js,加载进页面. <!DOCTYPE html> <html> <head> ... < ...

  3. Beta版本冲刺Day5

    会议讨论: 628:配置java环境已经成功,Tomcat部署也成功了,Mysql还未进行配置.601:继续修改其他的页面外观. 528:继续完成其他的功能页面. 340:对一些页面进行了优化美观,并 ...

  4. HTML5 UI框架Kendo UI Web教程:创建自定义组件(三)

    Kendo UI Web包 含数百个创建HTML5 web app的必备元素,包括UI组件.数据源.验证.一个MVVM框架.主题.模板等.在前面的2篇文章<HTML5 Web app开发工具Ke ...

  5. UNIX/Linux进程间通信IPC---管道--全总结(实例入门)

    管道 一般,进程之间交换信息的方法只能是经由fork或exec传送打开文件,或者通过文件系统.而进程间相互通信还有其他技术——IPC(InterProcessCommunication) (因为不同的 ...

  6. 老男孩Python全栈开发(92天全)视频教程 自学笔记16

    day16课程内容: 装饰器: def outer(): x=10 def inner(): print(x) return innerouter()() #inner 是局部变量,10闭包:如果在一 ...

  7. K2签约龙光地产,为集团实现“千亿目标”保驾护航

    随着房地产行业步入成熟期,行业整合及转型速度变快,房企要在数字经济的背景下实现稳步发展,企业信息化建设是其中的重要一环.此次龙光地产选择与K2携手,用统一流程平台为集团保驾护航,向实现千亿目标迈进. ...

  8. hdu5029 树链剖分 + 线段树

      将树映射在线段上进行操作 然后每个 重链变成一个连续的区间 #include <iostream> #include <cstdio> #include <strin ...

  9. Ubuntu 12.04将默认集成Landscape管理套件【转】

    转自:https://imtx.me/archives/1702.html 今天,我像往常一样对我的Ubuntu 12.04 Beta进行了一次常规升级,然后我发现在系统设置当中多了一个图标,叫「Ma ...

  10. solidity合约面向对象

    1. 属性[状态变量]的访问权限 public  internal[合约属性默认的权限]  private 说明:属性默认访问全向为internal,internal和private类型的属性,外部是 ...