webpack性能优化
-
开发环境性能优化
- 优化打包构建速度
- HMR
- 优化代码调试
- source-map
- 优化打包构建速度
-
生产环境性能优化
- 优化打包构建速度
- oneOf
- babel缓存
- 多进程打包
- 优化代码运行的性能
- 文件资源缓存(chunk-chunkhash-contenthash)
- tree shaking
- code split
- 懒加载/预加载
- externals
- dll
- 优化打包构建速度
HMR
question:
每个模块变化时,webpack-dev-server都会重新构建
就算只更改css,也会重新构建,假如有一百个模块,那每次其中一个模块更改,那就重新构建,不友好
concept:
HMR: hot module replacement 热模块替换 / 模块热替换
作用: 一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块)极大提升构建速度
样式文件: 可以使用HMR功能,因为style-loader内部实现了
js文件: 默认没有HMR功能 --> 需要修改js代码,添加支持HMR功能的代码
注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。因为你处理了入口文件,其实也就相当于处理了所有模块--
html: 默认不能使用HMR功能,同时导致问题,html不能热更新了(本地发生改变,页面没有重新更新),不用做HMR功能
解决: 修改entry入口,将html文件引入
开启HMR:
// devServer只在开发环境生效
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 当我们修改webpack配置,新配置要想生效,就必须重启webpack服务
hot: true
}
JS开启HMR:
// 一旦module.hot 为true,说明开启了HMR功能 --> 让HMR功能代码生效
if(module.hot) {
// 方法会监听 print.js 文件的变化,一旦发生变化,其他默认不会重新打包构建
// 会执行后面的回调函数
// 如果有其他模块,则继续写下面这个监听
module.hot.accept('./print.js', function() {
print()
})
}
开启HMR,解决html不能热更新的问题:
// 修改entry的入口,将html引入
entry: ['./src/index.js', './src/index.html']
source-map
// 开启source-map(最外层级)
devtool: 'source-map'
vue脚手架以及react用的是 eval-source-map / source-map
source-map: 一种提供源代码到构建后代码的映射技术,如果构建后的代码出错,通过映射可以找到源代码错误位置
source-map: 外部
inline-source-map: 内联
1、只生成一个内联source-map
2、错误代码准确信息 和 源代码的错误位置
hidden-source-map: 外部
错误代码错误原因,但是没有错误位置
不能追踪到源代码,只能提示到构建后代码的错误位置
eval-source-map: 内联
1、每一个文件都生成对应的source-map, 都在eval
2、错误代码准确信息 和 源代码的错误位置+hash值
nosources-source-map: 外部
错误代码准确信息,但是没有任何源代码信息
cheap-source-map: 外部
错误代码准确信息 和 源代码的错误位置
只能精确到行(整行标红) 其他可以精确到行列(就是将错误的位置标红)
cheap-module-source-map: 外部
错误代码准确信息 和 源代码的错误位置
module会将别人loader的source map加入
内联 和 外部的区别: 1、外部生成了文件,内联没有 2、内联构建速度更快
需求:
开发环境: 速度快,调试更友好
速度快(eval>inline>cheap...)
eval-cheap-source-map
eval-source-map
调试更友好
source-map
cheap-module-source-map
cheap-source-map
总结
eval-source-map / eval-cheap-module-source-map
生产环境: 源代码要不要隐藏?调试要不要更友好
内联会让代码体积变大,所以在生产环境不要用内联
隐藏:
nosources-source-map 全部隐藏
hidden-source-map 隐藏源代码 会提示构建后代码错误信息
调试友好
source-map / cheap-module-source-map
oneOf
module: {
rules: [
{
...同类型的文件loader处理
},
{
// 以下loader只会匹配一个,匹配到了就跳出去
// 注意: 不能有两个配置处理同一种类型文件,如果还有同类项的需要处理,则放在外面
oneOf: [
{
test: /\.css$/,
use: [...commonCssLoader]
},
{
test: /\.less$/,
use: [...commonCssLoader, 'less-loader']
}
]
}
]
},
缓存
babel缓存(打包时缓存有利于加快打包速度)
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
],
// 开启babel缓存
// 第二次构建时,如果文件没改动,会读取之前的缓存
cacheDirectory: true
}
},
文件资源缓存(让代码上线运行缓存更好使用)
hash: 每次webpack构建时会给一个唯一的hash值(每次打包都不一样),浏览器就不会缓存具体看打包后的文件名
问题:因为js跟css同时使用hash值
如果重新打包,会导致所有缓存失效,浏览器就的重新请求服务器
chunkhash:根据chunk生成的hash值,如果打包来源同一个chunk,那么hash值就一样
问题:因为css是在js中被引入的,所以同属于一个chunk
chunk: 一个js内引用的其他依赖,打包后同属于一个chunk
contenthash: 根据文件的内容生成hash值,不同文件的hash一定不一样
只要内容不变,打包后的hash值不变,
比如css改变,js不改变,则打包后css的hash改变,js不变,所以有利于浏览器缓存
demo:
output: {
filename: 'js/built.[contenthash:10].js',
path: resolve(__dirname, 'build')
},
tree shaking
作用: 去除无用的代码
前提:1. 必须使用ES6模块化
2. 开启production环境
作用:减少代码体积
在package.json中配置
// 所有代码都没有副作用(都可以进行tree shaking)
问题: 可能会把css @babel/poly(副作用)文件干掉
'sideEffects': false
解决:
'sideEffects': ["*.css", ".less"]
code split
通过入口分割chunk
// 单入口 -> 单页应用
// entry: './src/index.js',
entry: {
// 多入口: 有一个入口,最终输出就有一个bundle -> 多页应用
main: './src/index.js',
test: './src/test.js'
},
output: {
// [name]: 取文件名,就是入口设置的别名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
}
配置optimization(比较常用)
/**
*
* 1、(单、多)可以将引用的node_modules中库单独打包成一个chunk最终输出
* 2、自动分析多入口chunk中,有没有公共的依赖
* 如果有,这会单独打包成一个chunk,大于30kb才会分割
*
*/
optimization: {
splitChunks: {
chunks: 'all'
}
},
通过import动态语法单独打包chunk
原本:
import { mul } from './test.js'
import改动:
/**
* 单入口 想要让某个文件单独打包
* 通过js代码,让某个文件被单独打包成一个chunk
* import动态导入语法: 能将某个文件单独打包
*/
// 自定义打包后的文件名
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul }) => {
console.log(mul(1, 3));
})
.catch(error => {
console.log(error);
})
懒加载和预加载
/**
* 懒加载: 当文件需要使用时才加载,如果文件太大,那加载效果会有延迟
* 预加载 prefetch: 会在使用之前,提前加载js文件,两者效果一样
* 正常加载可以认为是并行加载(同一时间加载多个文件,同一时间加载越多时间越慢)
* 预加载是等其他资源加载完毕,浏览器空闲了,再偷偷加载
*
* 预加载在pc端高版本浏览器可以,但是在移动端兼容行不好,慎用(2022年不知道会不会)
*/
懒加载
document.querySelector('button').addEventListener('click', function() {
// 懒加载
// 这种语法打包构建一定会生成一个单独的chunk,才能被懒加载
// 点击按钮不会重复加载test,会用之前缓存的test
import(/* webpackChunkName: 'test' */'./test.js').then(({ mul }) => {
console.log(mul(3, 4));
})
})
预加载
document.querySelector('button').addEventListener('click', function() {
// 预加载
// 加载页面时,test就会预加载了,network可以看到,但是不会执行任何逻辑
// 按钮点击后就会调用这个预加载的文件
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test.js').then(({ mul }) => {
console.log(mul(3, 4));
})
})
多进程打包
对于打包资源比较大的项目,可以开启多进程打包;
使用的时候一定要放在对应的文件处理的最上边;
官网有对 thread-loader 配置详解
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/**
* 开启多进程打包
* 进程启动大概为600ms,进程通信也有开销
* 只有工作消耗比较大,才需要进行多进程打包
* 大项目开启 小项目不要
*/
{
loader: 'thread-loader',
options: {
workers: 3 // 进程3个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
],
// 开启babel缓存
// 第二次构建时,如果文件没改动,会读取之前的缓存
cacheDirectory: true
}
}
],
},
配置externals,使用CDN加载资源
在webpack.config.js中配置externals忽略打包,然后在html中手动引入CDN加载资源
externals: {
// 拒绝jQuery被打包进来
jquery: 'jQuery'
}
html中引入 CDN资源
dll单独打包(webpack3的东西,已经被弃用了)
code-split只能把引用的node_module的依赖打包成一个chunk,而dll可以把node_module的依赖先打包成单独的chunk,相关配置可以自行搜索
学习思想:手动创建并管理缓存
1、 第一次请求的时候,把请求后的内容存储起来
2、 建立一个映射表,当后续有请求的时候,先根据这个映射表看看请求的内容有没有被缓存,有的话就加载缓存,没有的话就走正常的请求流程(也就是缓存命中的问题)
3、命中缓存后,直接从缓存中拿取内容,交给程序执行
主要流程是上面这三步,更多的,可以加些权重,过期时间,多级缓存等等,一般我们在开发的时候,浏览器,http协议都帮我们把傻瓜卖弄这些操作封装好了。
wenpack dll需要手动去配置,很麻烦,后续有了AutoDLLPlugin插件,更是曾经被VueCli使用(后续被摒弃,因为作者觉得webpack4的打包性能已经优秀了,没必要使用这个插件),后续还有个hard-source-webpack-plugin