webpack 快速入门 系列 —— 性能
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> <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:懒加载其实用到的就是上文介绍的动态导入
预获取
思路可能是这样:
首先使用普通模式
普通模式下,一次性加载太多,而 a.js 这个文件又有点大,于是就使用懒加载,需要使用的时候在加载 a.js
触发点击事件,懒加载 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 }
来源https://www.cnblogs.com/pengjiali/p/15024605.html