【重学webpack系列——webpack5.0】
1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
从本节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
可以点击上方专栏订阅哦。
以下是本节正文:
webpack打包性能优化
1.优化大纲
weibapck性能优化主要可以考虑以下几方面
- 缩小查找范围,加快查找速度
- extensions
- alias
- modules
- mainFields
- mainFiles
- resolve
- resolveLoader
- noParse 不解析第三方库的第三方依赖
- DefinePlugin 创建编译时可以配置的全局变量,减少变量查找范围
- IgnorePlugin webpack 忽略某些模块,不把他们打包进去
- 净化css,抽离css
- thread-loader代替happypack,多进程处理
- cdn
- tree Shaking
- 代码分割
- 按需加载、提取公共代码
- scope Hosting 作用域提升
- 利用缓存
- babel-loader开启缓存
- 使用cache-loader
性能分析工具
- speed-measure-webpack-plugin 测试每个核心步骤耗费的事件
- webpack-bundle-analyzer 代码分析工具,生成代码体积等分析报告
2.优化详细说明
1.缩小查找范围,加快查找速度
1.1 extensions
有了这个extensions后,在
require
和import
的时候不需要加文件扩展名,会一次添加扩展名进行匹配
resolve: {
extensions: [".js",".jsx",".json",".css"]
},
1.2 alias
配置别名可以加快webpack查找模块的速度
不需要从
node_modules
文件夹中按模块的查找规则查找
const bootstrap = path.resolve(__dirname,'node_modules/_bootstrap@3.3.7@bootstrap/dist/css/bootstrap.css');
resolve: {
alias:{
"bootstrap":bootstrap
}
},
1.3 modules
modules
字段指定第三方模块的查找目录
// 默认是查找node_modules,但是会类似Nodejs一样的路径进行搜索,一层一层网上找node_modules
resolve: {
modules: ['node_modules'],// 先当当前目录下的node_modules,找不到找上层目录的node_moudles,直到全局的node_modules
}
// 如果确定依赖模块在项目根目录下的node_modules中,那么可以写绝对路径确定查找范围
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 确定查找目录就是项目下的node_modules,找不到不会往上层找
}
1.4 mainFilds和mainFiles
resolve: {
// 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
mainFields: ['browser', 'module', 'main'],
// target 的值为其他时,mainFields 默认值为:
mainFields: ["module", "main"],
}
- 这里的mainFileds代表了一个包解析入口文件应该看的字段,按照上面代码的顺序查找
resolve: {
mainFiles: ['index'], // 你可以添加其他默认使用的文件名
},
- 这里的mainFiles代表入口文件,如果mainFileds对应的字段没有,那么就看mainFiles对应的入口文件,如果mainFiles也没有,那么就是index
问:解析一个包或模块,如何找到入口文件面试点
- 先找到包/模块下对应的
package.json
中的main
字段,如果存在此字段,直接找到了,就直接返回。 - 如果没有这个字段,就找对应mainFiles,默认就是index.js
1.5 resolveLoader
用于配置解析loader时的resolve,默认配置:
module.exports = {
resolveLoader: {
modules: [ 'node_modules' ],
extensions: [ '.js', '.json' ],
mainFields: [ 'loader', 'main' ]
}
};
resolve与resolveLoader对象的key是一样的,后者是专门用于查找loader的
2.noParse 忽略对指定第三方模块的第三方依赖的解析
比如:
import jq from 'jquery'
当解析jq的时候,会去解析jq这个库是否有依赖其他的包
但是,如果配置了
noParse
,那么就不需要再去解析jquery中的依赖库了,这样能够增加打包速率。
module:{
noParse:/jquery/,//不去解析jquery中的依赖库
rules: [
...
]
}
3.DefinePlugin 定义全局变量
创建一些在编译时可以配置的全局变量,在编译的时候会直接替换掉,不需要再去查找
let webpack = require('webpack');
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: "1",
EXPRESSION: "1+2",
COPYRIGHT: {
AUTHOR: JSON.stringify("yh")
}
})
console.log(PRODUCTION);
console.log(VERSION);
console.log(EXPRESSION);
console.log(COPYRIGHT);
- 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值
- 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 ‘true’
- 如果配置的是一个对象字面量,那么该对象的所有 key 会以同样的方式去定义
- JSON.stringify(true) 的结果是 ‘true’
4.IgnorePlugin 忽略某些模块,不把他们打包进去
忽略第三方包的指定目录,让这些指定目录不要被打包进去。比如moment包,我只用中文的,那么可以用这个插件指定只打包中文的目录,其他语言就不需要打包了。加快了打包速度,减少了打包体积。
18、webpack优化(3)——IgnorePlugin_俞华的博客-CSDN博客_ignoreplugin
5.purgecss-webpack-plugin净化css,mini-css-extract-plugin压缩并抽离css
purgecss-webpack-plugin净化css,mini-css-extract-plugin压缩并抽离css,两者需要配合使用
- 使用:
const path = require("path");
+const glob = require("glob");
+const PurgecssPlugin = require("purgecss-webpack-plugin");
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PATHS = {
src: path.join(__dirname, 'src')
}
module.exports = {
mode: "development",
entry: "./src/index.js",
module: {
rules: [
{
test: /\.js/,
include: path.resolve(__dirname, "src"),
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
+ {
+ test: /\.css$/,
+ include: path.resolve(__dirname, "src"),
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: MiniCssExtractPlugin.loader,
+ },
+ "css-loader",
+ ],
+ },
],
},
plugins: [
+ new MiniCssExtractPlugin({
+ filename: "[name].css",
+ }),
+ new PurgecssPlugin({
+ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
+ })
],
};
6.thread-loader代替happypack,多进程处理
把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行
module.exports = {
mode: "development",
entry: "./src/index.js",
module: {
rules: [
{
test: /\.js/,
include: path.resolve(__dirname, "src"),
use: [
+ {
+ loader:'thread-loader',
+ options:{
+ workers:3
+ }
+ },
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
{
test: /\.css$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
"css-loader",
],
},
],
}
};
7.CDN 内容分发网络
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。
问:怎么使用CDN提高性能,加快访问速度?面试题
- 首先,我们一般将入口html文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的url变成指向CDN服务器的地址
- 这样做,是因为cdn服务一般会给资源开启很长时间,例如用户从cdn上获取了index.html后,即使之后发布操作把index.html覆盖了,但是用户在很长一段时间内,依旧使用的是之前运行的版本,这会导致新发布不能立即生效,所以我们将index.html不开启缓存放在自己服务器上
- 然后,很对静态的js、css、图片等文件需要开启缓存,上传到CDN上去,并且给每个文件名带上由文件内容算出的hash值
- 开启缓存和上传到CDN是为了能够读取更快,加上hash是因为文件会随着内容而变化,只要文件内容变化,那么对应的url就会变化,那么就会重新下载,无论缓存时间有多长。这样能保证文件一更新,读取的就是最新的文件,文件不更新,读取的就是缓存。
- 启用CDN后,所有的相对路径都改成指向CDN服务器的绝对路径。通过webpack的publicPath可以设置。
{
output: {
path: path.resolve(__dirname, 'dist'),
+ filename: '[name]_[hash:8].js',
+ publicPath: 'http://img.aiqiyi.cn'
},
}
-
另外,会把不同的静态资源分散到不同的CDN服务上去,因为同一时候,针对同一域名的资源 并行请求是优先的。
-
但是,多个域名会增加域名解析时间,所以可以在HTML的HEAD标签中加入
link
标签去与解析域名,以降低域名解析带来的延迟。-
link
标签一般会有一个rel
属性,值为dns-prefetch
,代表dns预拉取,拉取的地址是href
属性的值,比如:<link rel="dns-prefetch" href="http://img.aiqiyi.cn">
-
8.TreeShaking 只打包用到的API
tree shaking就是只把用到的方法打入bundle,没用到的方法会
uglify
阶段擦除掉
-
原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量,所以只能用在esModule中(
面试点
)-
没有导入的API都不会被打包
-
代码不会被执行,不可到达的不会被打包
import a from 'a'if(false){ console.log(a) // 不会达到,所以a不会打包进来}
-
代码执行的结果不被用到,不会被打包
-
代码中只赋值,不使用的变量,不会被打包
-
9.代码分割
9.1 按入口点分割
多入口项目,入口chunks之间包含了重复的模块,那么这些重复的模块会被引入到各个bundle中,所以提取出重复的模块作为单独的bundle,能够提升减少打包体积,提升打包效率
9.2动态导入(懒加载,按需加载)
使用
import()
函数配合魔法注释
,代表按需加载,魔法注释
一样的会打成一个包
import(/* webpackChunkName: "title" */ "./components/Title")
9.3预先加载preload和预先拉取prefetch
9.3.1 预先加载preLoad
- preload-webpack-plugin 能够将资源加载通过preload的方式加载,也就是预先加载
- 举例如下:
- 现在需要preload的地方,添加魔法注释"/* webpackPreload: true*/"(图1-1)
- 然后webpack配置插件(图1-2)
- 然后会发现,原先按需加载的权重应该是Low,但是现在变成了Hight(图1-3)
- 从html发现,还没点击按钮,已经插入了link标签,也就是资源已经被预先加载了(图1-4)
- 举例如下:
(图1-1)
(图1-2)
(图1-3)
(图1-4)
- 上面的html可以看出,link标签不止可以引出css,还可以引入script,只要写一个as=“script”,应该还可以引入其他类型。另外,上面的rel="preload"表示预加载
9.3.2预先拉取prefetch
- prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" href="utils.js" as="script">
button.addEventListener('click', () => {
import(
`./utils.js`
/* webpackPrefetch: true */
/* webpackChunkName: "utils" */
).then(result => {
result.default.log('hello');
})
});
9.3.3 preload 与 prefetch 的区别
-
preload告诉浏览器我马上要用到这个资源,请提高加载优先级为high,预先加载
-
prefetch告诉浏览器我未来可能会用到这个资源,请浏览器优先级设为lowest,在浏览器空闲的时候加载
- preload不要轻易用,因为会阻塞主要模块的加载,所以慎用。只有最关键最急需的资源才用。
9.4 提取公共代码
-
问:以下模块依赖图,由page1、page2、page3三个入口,实线代表依赖,比如page1依赖了module1,虚线代表动态导入,比如page1动态导入asyncModule1.js。请问:利用你觉得最佳的优化方案,会产出哪些文件?
-
思路:
-
提取第三方模块 jquery
会提取出一个endorspage1page2~page3.chunk.js
-
提取公共模块 module1 module2 module3
会提取出page1page2.chunk.js、page1page2~page3.chunk.js
-
提取动态加载的模块(import()),动态模块也按照1 2 3步骤提取
会提取出asyncModule1.chunk.js,然后还会提取出动态加载的模块的第三方依赖vendors~asyncModule.chunk.js,然后去提供公共模块,因为这里没有,所以么有产出
-
提取出入口文件
会提取出page1.js、page2.js、page3.js
-
-
-
问:module3在哪个产出文件中?
- 答:在page3.js中。因为如果一个模块被两个或两个以上引用,那么会单独打包出一个bundle,如果只有被一个引用,那么就会打包到引用方的包中。
-
分包是什么意思?
分包就是提取多个入口之间的公共模块,一般用于mpa多页应用
那么单页应用如何分包?通过懒加载import实现分包
-
代码分割的配置应该怎么配?
应该在webpack配置文件的optimization属性的splitChunks属性中配置
module.exports = {
optimization: {
splitChunks: {
chunks: "all", //代码分割应用于哪些情况,默认作用于异步chunk,值为all(同步+异步)/initial(同步import a from './a')/async(异步import())
minSize: 0, //分割出去的代码块最小的尺寸多大,代码块的最小尺寸,小于这个尺寸的就没必要分割出来,太小了,默认值是30kb,0就是不限制
minChunks: 2, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 3, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
maxInitialRequests: 5, //限制入口的拆分数量
name: false, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~,值可以是false、string或function,不能是true
automaticNameDelimiter: "~", //分隔符,默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: {// 将多个chunk的缓存合并成一个组
//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "all",
test: /node_modules/, //条件,如果这个模块request路径(绝对路径)里面包含node_modules,那么就属于第三方模块
priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
default: {
chunks: "all",
minSize: 0, //最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20,
reuseExistingChunk: false
}
// 有些模块,比如jquery,按照上面的配置,它会同时属于vendors和default两个缓存组,那么到底属于哪个缓存组呢,根据配置中的priority值大小来确定。
},
runtimeChunk:true // 运行时代码块单独打包成一个文件
},
}
-
上面的缓存组中,为什么priority要配置成负数?
- 答:因为webpack有默认缓存组,默认缓存组优先级为0。
- 如果你想要优先级比默认的要高,用正数
- 比默认的要低,就用负数。(负数的话,就是不覆盖默认配置)
- 答:因为webpack有默认缓存组,默认缓存组优先级为0。
-
多页应用html怎么配置?
- 根据下图配置,但是需要配置chunks,代表page1.html引用对应自己的chunks。
10.scope Hosting 作用域提升
Scope Hosting作用于提升,可以让 Webpack 打包出来的代码体积更小、运行速度更快。这是webpack3退出的功能。
-
为什么Scope Hosting能够减少代码体积、加快运行速度?
答:举个例子说明下:
如果文件a导出了字符串a,index.js引入了a,并且console.log(字符串a),那么如果没有开启
scope hosting
,打包出来的结果是:在modules数组中,有一个对象元素,key为a模块路径,value为一个函数,函数内部包含了a模块的内容;然后还有一个index.js模块,根据引用关系去找到modules中的a模块,然后加载进来。相当于打包后,a模块的内容都被打包进去了。
但其实,我只是在index.js中使用了一下a模块的导出结果,也就是字符串a,那么我只要把index.js中用到a的地方替换成字符串’a’就可以了,没必要把整个a模块都打包进来。
scope hosting
就是用来做到这一点的。
// a.js
export default 'a';
// index.js
import a from './a.js';
console.log(a);
// 不开启scope hosting打包出来的结果
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var a = ('a');
console.log(a);
})
// 开启scope hosting打包出来的结果
(() => {"use strict";console.log("a")})
11.利用缓存
webpack利用缓存来优化一般有2种思路:
- babel-loader开启缓存
- 使用cache-loader
11.1babel-loader
- Babel在转义js文件过程中消耗性能较高(语法树解析啥的),将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗
{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: "babel-loader",
options: {
cacheDirectory: true
}
}]
},
11.2cache-loader
- 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里
- 存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader
const loaders = ['babel-loader'];
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'cache-loader',
...loaders
],
include: path.resolve('src')
}
]
}
}
12.oneOf
一般来说,一个类型的文件只对应一个rule,但是webpack编译的时候,会对每个rules中的所有规则都遍历一遍,匹配test规则的整理出来,然后去执行laoder。
但是,如果用了oneOf,只要匹配test,匹配到一个,那么后面的loader就不再处理了。
也正是一个只匹配一个,所以oneOf中不能两个配置处理同一种类型的文件。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
//优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// 以下 loader 只会匹配一个
oneOf: [
...,
{test: /js/, ...}, // 匹配到一个,后面就不会再去匹配了
{test: /css/, ...}
]
}
]
}
}
3.性能分析工具
1. speed-measure-webpack-plugin 测试每个核心步骤耗费的时间
- 使用:在module.exports导出的内容外包一层wrap函数即可。
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports =smw.wrap({
});
- 结果:可以看到每个步骤、每个loader、plugin等消耗的时间
2.webpack-bundle-analyzer
webpack-bundle-analyzer是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能
上面这个插件的用法改了…
耗时分析
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');const smw = new SpeedMeasureWebpackPlugin();module.exports =smw.wrap({ ...});
- 可以看到每个步骤、每个loader、plugin等消耗的时间
webpack打包文件分析工具webpack-bundle-analyzer
webpack-bundle-analyzer是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
plugins: [
new BundleAnalyzerPlugin() // 使用默认配置
// 默认配置的具体配置项
// new BundleAnalyzerPlugin({
// analyzerMode: 'server',
// analyzerHost: '127.0.0.1',
// analyzerPort: '8888',
// reportFilename: 'report.html',
// defaultSizes: 'parsed',
// openAnalyzer: true,
// generateStatsFile: false,
// statsFilename: 'stats.json',
// statsOptions: null,
// excludeAssets: null,
// logLevel: info
// })
]
}
// 配置脚本
{
"scripts": {
"generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
"analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 启动展示打包报告的http服务器
}
}