webpack优化配置
1、开发环境性能优化
1.1 HMR(模块热替换)
hot module replacement 热模块替换 / 模块热替换
作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块)极大提升了构建速度
代码:只需要在 devServer 中设置 hot 为 true,就会自动开启HMR功能(只能在开发模式下使用)
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 修改webpack配置,新配置要生效,需重新webpack服务
hot: true
},
- 样式文件:可以使用HMR功能,因为style-loade内部实现了
- js文件:默认不能使用HMR功能
实现HMR需修改js代码,添加支持HMR功能代码(注意:HMR功能只能处理非入口js文件的其他文件 )
// module.hot为true 说明开始了HMR功能 => 让HMR功能代码生效
if (module.hot) {
// 此方法监听print.js文件的变化,一旦发生变化,其他模块不会重新打包构建,会执行后面的回调函数
module.hot.accept('./print.js',function(){
print()
})
}
- html文件:默认不能使用HMR功能,同时会到导致 问题:html文件不能热更新了
解决:修改entry入口,将html文件引入(不需要HMR功能,只有一个文件,不需要优化)
entry: ['./src/index.js','./src/index.html'],
1.2 source-map
source-map: 一种提供源代码到构建后代码映射技术(如果构建后代码出错了,通过映射可以追踪源代码错误)
参数:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
devtool: 'source-map'
- source-map:外部,错误代码准确信息 和 源代码的错误位置
- inline-source-map:内联,只生成一个内联 source-map,错误代码准确信息 和 源代码的错误位置
- hidden-source-map:外部,错误代码错误原因,但是没有错误位置(为了隐藏源代码),不能追踪源代码错误,只能提示到构建后代码的错误位置
- eval-source-map:内联,每一个文件都生成对应的 source-map,都在 eval 中,错误代码准确信息 和 源代码的错误位
- nosources-source-map:外部,错误代码准确信息,但是没有任何源代码信息(为了隐藏源代码)
- cheap-source-map:外部,错误代码准确信息 和 源代码的错误位置,只能把错误精确到整行,忽略列
- cheap-module-source-map:外部,错误代码准确信息 和 源代码的错误位置,module 会加入 loader 的 source-map
内联 和 外部区别:1、外部生成文件build.js.map,内联没有 2、内联构建速度更快
开发/生产环境可做的选择:
开发环境:需要考虑速度快,调试更友好
- 速度快( eval > inline > cheap >… )
eval-cheap-souce-map
eval-source-map - 调试更友好
souce-map
cheap-module-souce-map
cheap-souce-map
最终得出最好的两种方案:
- eval-source-map(完整度高,内联速度快)(vue react 脚手架默认使用)
- eval-cheap-module-souce-map(错误提示忽略列但是包含其他信息,内联速度快)
生产环境:需要考虑源代码要不要隐藏,调试要不要更友好
内联会让代码体积变大,所以在生产环境不用内联
隐藏源代码
- nosources-source-map 全部隐藏
- hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
最终得出最好的两种方案
- source-map(最完整)
- cheap-module-souce-map(错误提示一整行忽略列)
2、生产环境性能优化
2.1 oneOf
根据文件类型加载对应的loader,只要能匹配一个即可退出,优化打包速度。
module:{
rules:[{
oneOf:[
{
test:/\.css$/,
use:[...commonCssLoader]
},
{
test:/\.less/,
use:[
...commonCssLoader,
'less-loader'
],
}
]
}]
}
2.2 缓存
babel缓存:将 babel 处理后的资源缓存起来(哪里的 js 改变就更新哪里,其他 js 还是用之前缓存的资源),让第二次打包构建速度更快
{
loader:'babel-loader',
options:{
// 预设:指示babel做怎样的兼容性处理
presets:[
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns:'usage',
// 指定core-js版本
corejs:{
version:2
},
// 指定兼容性做到那个版本浏览器
targets:{
chrome:'60',
firefox:'60',
ie:'9',
safari:'10',
edge:'17'
}
}
]
],
// 开启babel缓存,第二次构建会读取之前的缓存
cacheDirectory: true
}
}
文件资源缓存:
- hash:每次webpack构建是会生成唯一的hash值
问题:因为js跟css同时使用一个hash值,重新打包,所有文件的 hsah 值都改变,会导致所有缓存失效。(可能只改动了一个文件) - chunkhash:根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样
问题:js跟css的hash值还是一样的因为css是在js中被引入的,所以同属于一个chunk - contenthash:根据文件的内容生成hash值,不同文件hash值不一样 => 让代码上线运行缓存更好使用
2.3多进程打包
多进程打包:某个任务消耗时间较长会卡顿,多进程可以同一时间干多件事,效率更高。
优点:提升打包速度,缺点:是每个进程的开启和交流都会有开销(babel-loader消耗时间久,所以使用thread-loader针对其进行优化)
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/*
thread-loader会对其后面的loader(这里是babel-loader)开启多进程打包。
进程启动大概为600ms,进程通信也有开销。(启动的开销比较昂贵,不要滥用)
只有工作消耗时间比较长,才需要多进程打包
*/
{
loader: 'thread-loader',
options: {
workers: 2 // 进程2个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
}
]
},
2.4 externals
externals:让某些库不打包,通过 cdn 引入
externals: {
// 拒绝jquery被打包,在index.html中通过cdn引进<script src="cdn引入">
// 写法:忽略库名:pm包名
jquery: jQuery
}
2.5 dll
dll:让某些库单独打包,后直接引入到 build 中。可以在 code split 分割出 node_modules 后再用 dll 更细的分割,优化代码运行的性能。
webpack.dll.js 配置:(将 jquery 单独打包)
/*
node_modules的库会打包到一起,但是很多库的时候打包输出的js文件就太大了
使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
当运行webpack时,默认查找webpack.config.js配置文件
需求:需要运行webpack.dll.js文件
--> webpack --config webpack.dll.js(运行这个指令表示以这个配置文件打包)
*/
const { resolve } = require('path');
const { webpack } = require('webpack');
module.exports = {
entry: {
// 最终打包生产的name:要打包的库为jquery
jquery: ['jquery']
},
output: {
// 输出出口指定
filename: '[name].js', //name就是jquery
path: resolve(__dirname, 'dll'), // 打包到dll目录下
library:'[name]_[hash]', // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins:[
// 打包生成一个manifest.json ==> 提供jquery的映射关系,告诉webpack jquery之后不需要再打包和暴露内容的名称
new webpack.DllPlugin({
name:'[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};
webpack.config.js 配置:(告诉 webpack 不需要再打包 jquery,并将之前打包好的 jquery 跟其他打包好的资源一同输出到 build 目录下)
// externals: {
// // 拒绝jquery被打包,通过cdn引进<script src="cdn引入">
// // 写法:忽略库名:pm包名
// jquery: jQuery
// }
2.6 tree-shaking
tree sharking:去除无用代码
前提:1、必须使用es6模块化,2、开启production环境
作用:减少代码体积
在page.json中配置 sideEffects":false 所有代码都没有副作用,都可进行tree shaking
问题:可能会把css / @babel/polyfill 副作用文件干掉
解决:“sideEffects”:[".css",".less"]
2.7 code split(代码分割)
code split: 代码分隔 并行加载 速度更快
- 多入口拆分
entry:{
main: './src/js/index.js',
test: './src/js/tree.js',
},
output:{
// [name]:取文件名
filename:'js/[name].[contenthash:10].js',
path:resolve(__dirname, "build")
},
- optimization
/**
* 可以将node_modules中代码单独打包成一个chunk最终输出
* 自动分析多入口chunk中,有没有公共文件,如果有会单独打包成单独一个chunk
* */
optimization:{
splitChunks: {
chunks: 'all'
}
},
- import 动态导入语法
// import 动态导入语法:能将某个文件单独打包
import (/*webpackChunkName: 'tree' */'./tree')
.then((result)=>{
console.log(result)
}
).catch(()=>{
console.log('文件加载失败')
})
2.8 懒加载 | 预加载
正常加载:可以认为是并行加载,同一时间加载多个文件
懒加载:当文件需要使用时才加载
预加载:会在使用前提前加载js文件,等其他资源加载完毕,等浏览器空闲了,在偷偷加载资源
document.getElementById('btn').onclick = () => {
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./tree').then(({mul})=>{
// eslint-disable-next-line
console.log(mul(4,5))
})
};
2.9 PWA
PWA:渐进式web应用,使用 serviceworker 和 workbox 技术,离线可访问,兼容性较差
webpack.config.js中配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); // 引入插件
// plugins中加入:
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker
生成一个 serviceworker 配置文件
*/
clientsClaim: true,
skipWaiting: true
})
index.js 中还需要写一段代码来激活它的使用:
/**
* 1、问题:eslint 不认识 window navigator等全局变量
* 解决:需要修改package.json中eslintConfig配置
* "evn":{
* "browser":true 支持浏览器全局变量
* }
* 2、sw代码必须运行在服务器上
* => nodejs
* => npm i serve -g serve -s build启动服务器,将build目录下的所有资源作为静态资源暴露出去
* */
// 注册serviceWorker
// 处理兼用性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('sw注册成功');
}).catch(() => {
console.log('sw注册失败');
});
});
}