性能优化
-
加快打包构建速度
- 预构建
- 持久化缓存
-
代码拆分、分包,避免重复依赖打包
-
静态资源优化
-
减少打包文件的体积
- 引入 webpack-bundle-analyzer 分析打包后的文件,判断哪些包还可以拆分和优化
-
tree-shaking
-
dll动态链接库:一些第三方包可以配置external,通过CDN或者外部js引入包,而不用打包
- 如echarts, 可通过自定义构建echarts,减少echarts体积,而不用使用npm来下载,通过外部js文件来引入。
dll动态链接库
项目中,框架vue、react、lodash等公共库,体积大,构建慢,较稳定,不经常升级版本
- 只需要一次构建就好,不需要每次都构建,完全没必要,会导致构建速度慢
- DLLPlugin 是 webpack 官方提供的一个插件,也是用来分离代码的
- DllPlugin -> 打包出dll文件
- dll是针对项目的性能优化的
- 使用 DLLPlugin 在打包的时候生成一个 manifest 文件,通过dll分包, webpack 会根据这个 manifest 文件的信息,分析出哪些模块无需打包
webpack.dll.config.js:
module.exports = {
name: 'vendor',
// entry: ['lodash','axios','react','react-dom','mobx'], // 作为公共类库
entry:{
// 将React相关模块放在一个单独的文件里 (动态链接库)
react:['react','react-dom']
},
output: {
path: path.resolve(__dirname, "dist/dll"),
// filename: "vendor.js",
filename:'[name].dll.js' // name对应entry的key
library: "_dll_[name]" // 打包后对外暴露的类库名称
},
plugins: [
new webpack.DllPlugin({
name: '_dll_[name]',
path: path.resolve(__dirname, "[name].manifest.json"), // 使用 DLLPlugin 在打包的时候生成一个 manifest 文件
})
// 告诉webpack使用了哪些动态链接库
new DllReferencePlugin({
manifest:path.resolve(__dirname, "react.manifest.json")
})
],
}
你会发现构建结果的应用代码中不包含 lodash,react,react-dom等公共类库 的代码内容,这一部分代码内容会放在 vendor.js 这个文件中,而你的应用要正常使用的话,需要在 HTML 文件中按顺序引用这两个代码文件,如:
<script src="vendor.js"></script>
<script src="react.dll.js"></script>
<script src="main.js"></script>
在构建的时候,我们需要优先使用 webpack.dll.config.js 来打包,如 webpack -c webpack.dll.config.js --mode production,构建后生成公共代码模块的文件 vendor.js 和 manifest.json,然后再进行应用代码的构建
DLLPlugin 构建出来的内容无需每次都重新构建,后续应用代码部分变更时,你不用再执行配置为 webpack.dll.config.js 这一部分的构建,沿用原本的构建结果即可,所以相比 optimization.splitChunks,使用 DLLPlugin 时,构建速度是会有显著提高的
注意:
- 要使用dll分包,需要优先使用
webpack.dll.config.js来打包生成对应的manifest.json和vendor.js - DLLPlugin 构建出来的内容无需每次都重新构建
- 当你升级 lodash(即你的公共部分代码的内容变更)时,要重新去执行
webpack.dll.config.js这一部分的构建,不然沿用的依旧是旧的构建结果
DLLPlugin/DllReferencePlugin
DLLPlugin :作用就是把第三方库和代码分开,vendor.dll.js文件里包含的就是第三方库的文件。-
- DLLPlugin: 打包出dll文件
DllReferencePlugin:用于在Webpack配置中引用这些预编译的DLL文件。生成动态链接库
- DllReferencePlugin: 使用dll文件
注意:webpack.dll.config.js打包生成的两个文件,一个vendor.dll.js,另一个是vendor-manifest.json。前一个是保存的第三方库文件,后一个是库文件的索引。DllReferencePlugin插件读取vendor-manifest.json文件,把库文件映射到相关的依赖上去。
代码分割/代码拆分
为什么CSS 文件分离出来,而不是直接一起打包在 JS 中?
- 主要的原因是我们希望更好地利用缓存
1、假设静态资源都打包成一个js文件,加载页面时虽然只需要加载一个 JS 文件,但是我们的代码一旦改变了,用户访问新的页面时就需要重新加载一个新的 JS 文件。有些情况下,我们只是单独修改了样式,这样也要重新加载整个应用的 JS 文件,相当不划算
2、还有一种情况是我们有多个页面,它们都可以共用一部分样式(这是很常见的,CSS Reset、基础组件样式等基本都是跨页面通用),如果每个页面都单独打包一个 JS 文件,那么每次访问页面都会重复加载原本可以共享的那些 CSS 代码
如果分离开来,第二个页面就有了 CSS 文件的缓存,访问速度自然会加快。虽然对第一个页面来说多了一个请求,但是对随后的页面来说,缓存带来的速度提升相对更加可观
-
optimization.splitChunks
- 公共引用的模块(库)帮单独提出来做下代码分割
- 自动生成了一个
verdor~main.js文件
optimization: {splitChunks: {chunks: "all" //所有的 chunks 代码公共的部分分离出来成为一个单独的文件}}
我们需要在 HTML 中引用两个构建出来的 JS 文件,并且 commons.js 需要在入口代码之前。下面是个简单的例子:
<script src="commons.js" charset="utf-8"></script>
<script src="entry.bundle.js" charset="utf-8"></script>
使用了 html-webpack-plugin,那么对应需要的 JS 文件都会在 HTML 文件中正确引用
将公共使用的第三方类库配置为公共的部分
不应该由 webpack 自己去判断处理。因为公共的第三方类库通常升级频率相对低一些,这样可以避免因公共 chunk 的频繁变更而导致缓存失效
module.exports = {
entry: {
vendor: ["react", "lodash", "vue", ...], // 指定公共使用的第三方类库
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: "vendor",
name: "vendor", // 使用 vendor 入口作为公共部分
enforce: true,
},
},
},
},
// ... 其他配置
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /react|angluar|lodash/, // 直接使用 test 来做路径匹配
chunks: "initial",
name: "vendor",
enforce: true,
},
},
},
},
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: path.resolve(__dirname, "node_modules") // 路径在 node_modules 目录下的都作为公共部分
name: "vendor", // 使用 vendor 入口作为公共部分
enforce: true,
},
},
},
},
}
上述第一种做法是显示指定哪些类库作为公共部分,第二种做法实现的功能差不多,只是利用了 test 来做模块路径的匹配,第三种做法是把所有在 node_modules 下的模块,即作为依赖安装的,都作为公共部分。你可以针对项目情况,选择最合适的做法。
理解chunks
chunks 用以告诉 splitChunks 的作用对象,其可选值有 async、 initial 和 all。默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分
chunks值为all时,会比较强大,一般设置为all即可,处理范围包括了初始chunk和异步chunk两种场景
CSS分离
- mini-css-extract-plugin插件:将CSS分离成单独的文件
总结
MiniCssExtractPlugin插件来将CSS分离成单独的文件,而splitChunks选项用于提取公共模块splitChunks移除了重复的依赖模块, 减轻了大小。 【如lodash】
tree-shaking
webpack2.x开始⽀支持 tree shaking概念,顾名思义,"摇树
- 用于描述移除 JavaScript 上下文中的未引用代码
- 摇树 可以理解为 把没有用到的代码给移除下去,消除无用代码,不进行打包
sideEffects
- webpack 4之后可支持通过
package.json的"sideEffects"属性作为标记,可以指定项目中的副作用
"sideEffects": false
如果所有代码都不包含副作用,可以直接设置该属性标记为
false,来告知 webpack 它可以安全地删除未用到的export
- 可指定项目中文件模块中副作用,比如像全局的less文件导入,会影响到全局作用域,不应该被tree-shaking摇掉
"sideEffects": [
"*.less"
],
esbuild-loader提升构建速度
esbuild-loader是一个 Webpack 的 loader,可以使用esbuild作为 JavaScript 构建工具来加速构建速度esbuild是一个高性能的 JavaScript 构建工具,可以快速地处理 JavaScript 和 TypeScript 代码- 可以使用esbuild-loader替换babel-loader、ts-loader
可处理tsx/typescript/jsx/javascript 代码
esbuild-loader 可以自动检测 tsconfig.json文件
module.exports = {
module: {
rules:[
test: /\.js$/, // /\.tsx?$/、/\.js$/
loader: 'esbuild-loader',
options: {
loader: 'jsx', // tsx、jsx
target: 'es2015'
}
]
}
}
持久化缓存提高构建速度
持续化构建过程来说,第一次构建是一次全量构建,然后它会将相关产物序列化缓存在磁盘中(serialize)。后续构建具体流程可以依赖于上一次的缓存进行:读取磁盘缓存 -> 校验模块 -> 解封模块内容
持久缓存会将缓存存储到磁盘
小图片 base64编码
针对小图片/小图标的场景,比如小于5kb的可以进行base64格式产出
- 减少http请求,提高页面加载速度
Base64编码限制
- Base64编码会将图片转换为文本格式,增加了文件体积,对于大图片不适合使用。
- Base64编码后的文本会比原始图片文件稍微大一些,因为Base64编码使用了一些额外的字符表示二进制数据。
- Base64编码后的文本文件无法进行图片优化压缩,如gzip压缩,因为文本文件已经是经过编码的。
rules: [
{
test: /\.(png|jpg|gif)$/i, // 匹配图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 5 * 1024, // 小于 5KB 的图片将转为 base64 编码
name: '[name].[hash:5].[ext]', // 输出文件的命名规则
outputPath: 'images', // 输出路径
},
},
],
},
...
]
可以使用
url-loader和file-loader来将小图片转换为Base64编码。这两个加载器都是Webpack的一部分,url-loader是file-loader的上层封装
bundle加hash
根据文件的内容生成hash
- 文件内容变了才会生成hash,如果文件内容没有发生变化,webpack生成hash值是一样,根据浏览器的缓存机制,那么在加载资源会从缓存中获取,就不用重新加载一遍新资源
output:{
filename:'[name].[contentHash:8].js'
}
需要注意的是,对于不同的文件类型,Webpack提供了多种hash选项,如[hash]、[chunkhash]、[contenthash]等。其中,[hash]适用于整个构建过程,而[chunkhash]适用于每个独立的代码块,[contenthash]适用于每个文件的内容。使用[contenthash]可以确保只有文件内容变化时,hash值才会发生变化,从而更好地利用缓存。
利用了浏览器什么缓存机制?
利用了浏览器的强缓存和协商缓存机制。
- 当浏览器发起资源请求时,首先检查强缓存是否命中。如果强缓存命中,浏览器直接从缓存中获取资源,不发送请求。如果强缓存未命中,则发送请求,服务器会根据协商缓存机制判断资源是否有更新。如果资源没有更新,则服务器返回
304 Not Modified状态码,浏览器接收到这个响应后,从缓存中获取资源。如果资源有更新,则服务器返回新的资源内容
强缓存:响应头中设置
Cache-Control和Expires协商缓存 响应头:
Last-Modified和ETag
使用CDN
- 配置CDN域名前缀,通过pubic
- 需要将打包后的静态文件上次到CDN中
output:{
publicPath:'https//cdn.com' // 将静态文件url的前缀替换成cdn域名
}
- 也可以单独设置图片的cdn地址
如果设置了output的cdn域名,里面图片就不需要另外设置cdn地址了
use: [
{
loader: 'url-loader',
options: {
limit: 5 * 1024, // 小于 5KB 的图片将转为 base64 编码
name: '[name].[hash:5].[ext]', // 输出文件的命名规则
outputPath: 'images', // 输出路径
publicPath:'https//cdn.com' // 设置图片的cdn地址
},
},
],
Scope Hosting 【webpack已内置】
Scope Hosting是Webpack 4及以上版本的默认优化特性,不需要额外的配置来开启
可以减少生成的代码的模块和创建新的函数作用域,从而减少了在浏览器中运行时的内存消耗和性能开销
在传统的Webpack打包中,每个模块都会被包裹在一个函数作用域中,这意味着每个模块都有自己的作用域,并且模块之间的变量不会相互影响。这样的好处是模块之间的变量不会相互污染,但同时也带来了一些性能开销,因为需要在运行时维护多个函数作用域。
- Scope Hosting的目标是优化这一过程,将具有相同作用域的模块合并在一起,避免创建额外的函数作用域, 相同作用域内的变量可以共享,减少了不必要的闭包和内存占用。
- Scope Hosting的实现依赖于ES6模块系统(使用
import和export语法)
原理
- 当Webpack检测到某个模块没有被其他模块引用或被引用的次数为1时,它将会将这些模块合并在一起,并在同一个函数作用域内运行它们。
moduleA.js
jsCopy code
export const x = 10;
moduleB.js
jsCopy codeimport { x } from './moduleA';
console.log(x);
在传统的Webpack打包中,moduleA和moduleB都会被包裹在自己的函数作用域中。但是,如果启用了Scope Hosting,Webpack会将它们合并在同一个作用域中,从而减少函数的创建和内存占用。
external
1、通过CDN或者外部js引入包,而不用打包,提高打包优化速度
2、通过CDN引入包,这样可以减少bundle打包后的体积,也可以利用浏览器缓存,对于不经常变化的文件进行缓存,从缓存获取资源
- 指出应将哪些模块视为外部模块,外部模块不会被打包
- 防止将某些
import的包(package)打包到 bundle 中
- 防止将某些
external接受一个模块名称的数组或一个接受模块名称的函数,如果它被视为外部引用(externals)则返回 true
用法:
- 在index.html引入的脚本,如从CDN引入,或者已经打包好的js,这不需要重新打包
- 比如在inxex.html引入了lodash,external配置了lodash外部模块
- 则lodash不会被打包,可以在组件通过import引入lodash模块
按需加载、懒加载配置
路由懒加载
- import()语法实现动态导入
- vue、react异步组件
- 结合vue-router 、react-router异步加载路由
- 是基于import语法实现
合理配置source-map
合理配置source-map, 在定位代码错误时使用,而不影响打包后的包体积大小
- 在webpack的devtool配置,有几个选项可开启source-map,通过配置