webpack性能优化:
开发环境性能优化
1)优化打包构建速度 —— HMR
2)优化代码调试—— source-map
生产环境性能优化
1)优化打包构建速度——oneOf、babel缓存、 多进程打包
2)优化代码运行的性能——文件缓存、tree shaking、code split、dll、懒加载和预加载、pwa、externals
开发环境配置–优化打包构建的速度
使用HMR(hot module repalcement),热模块替换,作用是一个模块发生改变,只会重新打包这个模块,而不是全部更新。极大地提升了构建速度。
**样式文件:**不必做特殊处理,使用的style-loader内部实现了热模块替换功能。
**js文件:**在默认情况下不会使用HMR功能,需要在js代码中添加支持HMR功能的代码。
if (module.hot) {
module.hot.accept('./logon.js', () => {
logon();
});
}
以上代码表示只有在logon模块发生改变时内部函数才会执行处理。其他模块不会重新打包构建,这段代码在那个中监听的logon模块在哪个js文件中引入,就在哪个js文件中添加即可。注意:HMR功能只能对非入口文件进行处理,入口文件无法处理。一旦入口文件被修改,整个项目代码都会被重新构建。
**html文件:**不能使用hot功能,需要在webpack配置入口修改配置,将html文件作为入口文件引入。但是修改完成之后,一旦html文件发生变化,所有文件都会被重新编译加载。
entry:["./src/js/index.js","./src/index.html"],
开发环境配置–优化代码调试
使用工具:source-map——提供一种源代码到构建后代码的映射技术,如果构建后代码出错,可以通过追踪源代码错误信息的位置。需要在webpack.config.js中新增配置项:
devtool:"source-map"
新增的配置会在构建后的输文件夹中输出对应的sourceMap文件,以方便在浏览器中通过源码进行调试,devtool取值的不同输出的sourceMap输出的方式也不同,关于devtool的取值如下如所示:
devtool | 含义 |
---|---|
空 | 不生成 Source Map,错误代码提示准确,没有任何的源代码信息 |
eval | 每个 module 会封装到 eval 里包裹起来执行,并且会在每个 eval 语句的末尾追加注释 //# sourceURL=webpack:///./main.js |
source-map | 会额外生成一个单独 Source Map 文件,并且会在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map 。错误代码提示准确,并且提示源代码的错误位置。 |
hidden-source-map | 和 source-map 类似,但不会在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map 。不能追踪源代码的错误位置,只能提示到构建后代码的错误位置 |
inline-source-map | 和 source-map 类似,但不会额外生成一个单独 Source Map 文件,而是把 Source Map 转换成 base64 编码内嵌到 JavaScript 中。错误代码提示准确,并且提示源代码的错误位置。 |
eval-source-map | 和 eval 类似,但会把每个模块的 Source Map 转换成 base64 编码内嵌到 eval 语句的末尾,例如 //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW... 。错误代码提示准确,并且提示源代码的错误位置。 |
cheap-source-map | 和 source-map 类似,但生成的 Source Map 文件中没有列信息,因此生成速度更快。错误代码提示准确,并且提示源代码的错误位置,只能精确到行,不能精确到列。 |
cheap-module-source-map | 和 cheap-source-map 类似,但会包含 Loader 生成的 Source Map错误代码提示准确,并且提示源代码的错误位置。 |
根据以上的配置,或生成独立的sourceMap文件或直接内敛至js文件的末尾。除了生成的sourceMap文件的不同之外,对源代码的映射也不同,即有错误可提示到源代码的具体位置,或是提示至打包构建后代码的位置。
打包后浏览器中sourceMap映射的源码:
生成单独map文件:
没有单独的sourceMap文件,内敛在js中的sourceMap:
那么问题来了,如何选择合适的source-map?
秉着开发环境构建速度更快,调试更加友好的原则,建议:
速度快(eval-source-map/eval-cheap-source-map),调试友好(source-map/cheap-module-source-map/cheap-sourcemap),所以在开发环境下把 devtool
设置成 cheap-module-eval-source-map
,因为生成这种 Source Map 的速度最快,能加速构建。由于在开发环境下不会做代码压缩,Source Map 中即使没有列信息也不会影响断点调试;
生产环境主要是考虑代码要不要隐藏,要不要做调试,因为内敛的sourceMap会让代码的体积变得巨大,所以内敛的方式都不会被采用,建议:
hidden-source-map 隐藏源代码,会提示构建后的代码错误信息。
生产环境需要调试的则直接使用source-map或者cheap-module-source-map
优化的配置项:
oneOf:优化module中loader的执行方式
如果你的module中配置了多个loader,webpack打包每一个文件的时候都会有序执行每一个loader配置,相当于每一个文件都会被所有的loader过一遍,会减缓webpack的打包速度。使用oneOf配置项可以使每一个文件只会匹配一个loader,oneof里面不能有两项配置处理同一个类型的文件:如果需要,则可以将其中一个优先执行的提取到oneOf外面进行配置:
module: {
//loader的详细规则
rules: [
{
test: /\.js$/,
loader: "eslint-loader",//js检查语法
exclude: /node_modules/,
enforce: 'pre',//优先执行
options: {
fix: true
}
},
{
//每一个文件只会匹配一个loader
oneOf: [
{
test: /\.css$/,
use: [{
loader: miniCssExtractPlugin.loader,
options: {
publicPath: "../",
},
},
{
loader: "css-loader"
},
{
loader: 'postcss-loader',
options: {
ident: "postcss",
plugins: [
require("postcss-preset-env")()
]
}
}
]
},
{
test: /\.js$/,
loader: "babel-loader",//js检查语法兼容
exclude: /node_modules/,
options: {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
]
},
],
}
例如以上的module配置,js文件我们需要语法检查和语法兼容,但是配置里又有css-loader,js文件是不需要css-loader进行处理的,所以将配置放在oneOf里面,表示每一个文件只用匹配一个loader进行处理。但是js文件需要进行两次处理,即可把其中一个优先执行的配置放在外面,在进行oneOf处理,在文件很多的大项目里,会大大提高webpack的打包速度。
生产环境的缓存配置:
1)babel缓存:babel是对js文件进行处理编译处理、兼容性处理,编译成浏览器能够识别的语法。假设js有很多个模块,若开发过程中只改变其中一个模块,编译时会重新打包所有模块,降低webpack的打包速度,跟HMR功能类似,只不过HMR是浏览器刷新时的重新加载的模块,这个指的是webpack打包时的模块。
开启babel缓存只需要在babel-loader下的options下面添加:cacheDirectory: true
{
test: /\.js$/,
loader: "babel-loader",//js检查语法兼容
exclude: /node_modules/,
options: {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: 'usage',
corejs: {
version: 3
},
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
],
//开启babel缓存
cacheDirectory: true
}
}
2)文件资源缓存:
从服务请求回来得到的资源一般都是有强制缓存的,这样会提高用户浏览网页的速度,当用户再次进入该页面时不用从服务器获取相应的资源,而是从缓存中获取。但是假设有网页有缺陷,开发人员紧急修复,但是用户有缓存效果没有得到最新的资源。则这不会是我们想要的效果。
基本思路:每个文件附带版本号,每次请求若版本号变化时,会重新获取资源而不是从缓存中获取。每一次webpack打包都会产生一个hash值,将此hash值作为输出文件的名称的拼接可以有效解决这个问题。其中hash值包括:
(1)hash:每次打包构建生成的唯一hash值。
(2)chunkhash:根据chunk生产的hash值,如果打包来源是属于同一个chunk,则它们使用的hash值就会是一样的。
(3) contenthash:根据文件内容产生的hash值,不同文件的hash值是不一样的。
这三个hash也是面试之中面试官常问的问题。那么我们应该用哪一个?用contenthash!
使用hash值每次打包,所有的缓存都会失效,但是发生改变的文件可能只有几个,但是又要从服务器中重新获取。
使用chunkhash和hash是一样的,若不同的文件来自同一个chunk,则修改其中一个重新打包,其他文件的缓存也会失效。所以真相只有一个,使用contenthash相对较好。
output: {
//输出的文件名字
filename: "js/built-[contenthash:12].js",
path: resolve(__dirname, "build")
},
总结:babel缓存让第二次打包构建的速度更快,文件资源缓存让上线运行环境的缓存更好使用。
tree shaking:去除无用代码,忽略一些无用文件(css\js)减小构建后文件的体积。
使用方式:使用ES6模块化,开启production模式。
在默认情况使用tree shaking时,不同的版本可能会忽略掉一些有用的文件,例如你在js中import了css文件,但是这个js文件的代码里并不会用到这个css文件,此时打包会误认为是无用的文件。所以必须在package.js中配置sideEffects
"sideEffects":["*.css"]//多个需要配置的文件就写在数组中。
code split 代码分割
将一个大的文件分割打包成多个文件,加载时可以进行并行加载速度更快,对于按需加载功能才可以实现。
实现方式:
1)多入口文件配置:
//使用多入口文件可以实现不同模块单独输出js文件
entry: {
index:"./src/js/index.js",
logon: "./src/js/logon.js"
},
2)使用optimization配置:可以将第三方文件也就是node_modules中的文件单独打包出来成为一个js文件,例如上述中有index.js文件和logon.js文件,加入两个文件都引入了同一个jquery模块,这是可以将jQuery单独打包成一个文件。如果不单独打包,两个文件内部都引入了jQuery,文件非常大,相当于打包了两次jQuery。在浏览页面的时候,jq文件存在的缓存也不会重复加载
optimization:{
splitChunks:{
chunks: "all"
}
}
//自动分析多入口文件中有没有公共的依赖,如果有,将这个依赖单独打包处理
3)使用js代码完成单独打包:
可以在js使用import引入文件的时候,通过内部js代码将引入的文件单独打包成一个js,例如你在index.js文件里引入了logon,但是你是单入口配置,在不修改配置的情况下,可以
import(/* webpackChunkName: 'logon' */'./logon').then(({logon})=>{
console.log(logon);
}).catch(()=>{
console.log("logon加载失败");
});
/* webpackChunkName: 'logon' */
表示输出文件的名字为logon
PWA 渐进式网络开发应用程序——离线访问,兼容性问题,还没完全普及。
使用workbox插件 workbox-webpack-plugin
按照惯先npm安装插件,然后引入并且在plugins配置项里new 一个:
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim:true,
skipWaiting: true,
})
//帮助serviceworker快速启动,删掉旧的serviceworker,生成一个serviceworker配置文件
兼容处理,在入口文件做处理,之后在服务器上打开即可
if("serviceWorker" in navigator){
window.addEventListener("load",()=>{
// service-worker.js是打包生产的配置文件
navigator.serviceWorker.register('/service-worker.js')
.then(()=>{
//注册成功处理函数
console.log("1");
})
.catch(()=>{
//注册失败处理函数
console.log("2");
})
})
}
打开浏览器即可在开发工具里看到已经注册好的serviceWorker
多线程打包—— thread-loader
webpack单线程处理程序,如果打包的文件很多,工程量比较大,则打包速度就会降低,所以此时可以考虑多线程打包。多进程打包是需要开启时间的,启动时间大约为600ms,进程通信为也需要时间,只有打包时间需要较长的打包才会使用多进程打包。
{
test: /\.js$/,
exclude: /node_modules/,
use:[
{
loader: "thread-loader",
workers: 2 //进程数,可以和cpu进程数比较
},
{
loader: "babel-loader",//js检查语法兼容
options: {
presets: [
[
"@babel/preset-env",
{
//......
}
]
],
cacheDirectory: true
}
}
],
}
externals——忽略某些包,不进行打包。
在打包的时候有些包是不需要打包到项目里面的,例如通过CDN进行引入的包,这个时候就可以使用externals
externals:{
//忽略的npm包名 不打包
jquery: "jQuery"
},
//在webpack.config.js的entry同级下配置这个选项,此时jq不会被打包到项目中
dll 动态链接库
将一些需要单独打包的库(一般为第三方库)一次打包完成,可以将多个库打包成一个chunk,后续修改代码也不用重新打包这个库。即:优化第三方库的打包。
操作步骤:
(1)新建一个新的打包配置文件,名字随意。
const {resolve} = require("path");
const webpack = require("webpack");
module.exports= {
entry:{
//最终打包生成的name 和 需要单独打包的库
jquery : ["jquery"] //如果有更多的库可以放在数组里面
},
output:{
filename: "[name].js",
path: resolve(__dirname,"dll"),
library: "[name]_[hash]" //打包的库里面向外暴露的内容名字
},
plugins:[
//打包生成一个manifest.json 停供映射的内容
new webpack.DllPlugin({
name: "[name]_[hash]", //映射的库暴露的名字
path: resolve(__dirname,"dll/manifest.json"),
})
],
mode: "production"
}
(2)使用npm命令打包第三方库 webpack --config webpack.dll.js
webpack.dll.js为新建的打包配置文件,此时输出目录下会有以上配置的输出文件manifest.json和jquery.js。
(3)在webpack.config.js里配置 告诉webpack不参与打包,同时哪些库的名字需要需改。同时需要使用add-asset-Html-webpack-plugin
插件,在打包后的html里自动引入这个第三方库。打包完成之后打开html文件会发现自动引入jquery.js文件
module.exports= {
//入口起点
entry:"./src/js/index.js",
output:{
filename: "js/built.js",
path: resolve(__dirname,"build")
},
plugins:[
new HtmlWbpackPlugin({
template: './src/index.html'
}),
//告诉webpack哪些库不参与打包,同时使用的名字也得变化
new webpack.DllReferencePlugin({
manifest: resolve(__dirname,"/dll/manifest.json")
}),
// 将打包的文件输出并在html中自动引入该资源
new addAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, "/dll/jquery.js")
})
],
mode: "production"
}