Webpack5学习笔记(四):Webpack优化配置
Author: 哇哇小仔
Date: 2021-03-18
Webpack版本:webpack 5.24.4
webpack-cli 4.5.0
说明:尚硅谷webpack视频教程总结的原创笔记
第5章:webpack优化配置
5.1 HMR
- HMR
- hot module replacement 热模块替换/模块热替换
- 作用:一个模块发生变化,只会重新打包这一个模块,而不是重新打包所有模块,极大提升构建速度
- 不同文件的处理方式
- css样式文件
- 可以使用HMR功能,因为style-loader实现了
- 这也是为什么开发模式使用style-loader,因为方便开发。生产模式为了效率,不需要HMR功能
- js文件
- 默认没有HMR功能
- 需要修改js代码,添加支持HMR功能的代码
- 注意:HMR功能对于js的处理,只能处理非入口的js文件
- html文件
- 默认没有HMR功能,同时会导致问题:html文件不能热更新了
- 解决:修改entry入口,将html文件引入
entry: ['./src/js/index.js', './src/index.html']
- html因为一个项目里边只有一个html文件,不需要做HMR功能
- css样式文件
// 比如修改入口的index.js文件
if (module.hot){
// 一旦module.hot为true,说明开启了HMR功能 --> 让HMR代码生效
module.hot.accept('./print.js', funtion(){
// 方法会监听print.js文件的变化,一旦发生变化,其他js模块不会重新打包构建
// 而只会重新加载print.js并执行这个回调函数
print();
})
}
- 开发模式下开启HMR模式
// 开启HMR只需要在devServer中设置hot属性为true即可
devServer: {
contentBase: resolve(__dirname, 'build');
compress: true,
port: 3000,
open: true,
hot: true // 开启HMR功能
}
5.2 source-map
- 简介:source-map 是一种提供源代码到构建后代码的技术,如果构建后代码出错了,会通过映射关系追踪到源代码的错误
- 启用source-map:
//在webpack.config.js中加入
devtool: 'source-map'
- source-map的类型:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
- 内联 和 外部 source-map 的区别
- 外部生成文件,内联没有
- 内联构建速度快
- 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加入
- source-map: 外部
- 开发环境:要求速度快,调试友好
- 速度:eval > inline > cheap
- eval-cheap-source-map
- eval-source-map
- 调试友好
- source-map
- cheap-module-source-map
- cheap-source-map
- 选择:
eval-source-map / eval-cheap-source-map
(react和vue用的都是eval-source-map)
- 速度:eval > inline > cheap
- 生产环境:源代码要不要隐藏?调试是否友好?
- 内联会让代码体积非常大,所以在生产环境不用内联
- nosources-source-map 全部隐藏
- hidden-source-map 只隐藏源代码,会提示构建后的代码错误
- 较好的选择:
source-map / cheap-module-source-map
5.3 oneOf
- 作用:优化生产环境的打包速度,oneOf中的loader每次只会有一个被执行
- 用法:
const { resolve } = require('path')
const { MiniCssExtractPlugin} = require('mini-css-extract-plugin')
const commonCssLoader = [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
ident: 'postcss',
plugins: ['postcss-preset-env']
}
}
}
module.exports = {、
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'build')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
enforce: 'pre'
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// 以下loader只会匹配一个
// 注意:不能有两个配置处理同一个类型的文件
// 因此,只能写一个js的处理loader在里边
oneOf: [
{
test: /\.css$/,
use: [...commonCssLoader]
},
{
test: /\.less$/,
use: [...commonCssLoader, 'less-loader']
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {version: 3},
targets: {
chrome: '60',
firefox: '50'
}
}
]
}
},
{
test: /\.(jpg|png|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
esModule: false,
outputPath: 'imgs'
}
},
{
test: /\.html$/,
loader: 'html-withimg-loader'
},
{
exclude: /\.(html|js|less|css|jpg|png|gif)$/,
loader: 'file-loader',
options: {
outputPath: 'media',
name: '[hash:10].[ext]'
}
}
}
]
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'}),
new MiniCssExtractPlugin({filename: 'css/built.css'}),
new OptimizeCssAssetsWebpackPlugin()
],
mode: 'production'
}
5.4 缓存
- babel缓存
- 设置:在babel-loader的options中添加
cacheDirectory: true
- 作用: 让第二次打包构建速度更快
- 设置:在babel-loader的options中添加
- 文件资源缓存
- hash:每次webpack打包构建时会生成一个唯一的hash值
- 问题:因为js和css同时使用一个hash值,如果重新打包,会导致所有缓存失效,但是我们可能只改动一个文件
- chunkhash: 根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样
- 问题:js和css的hash值还是一样的,因为css是在js中被引入的,所有属于一个chunk
- contenthash:会根据文件内容生成hash值,不同文件的hash值肯定是不一样的
- 让代码上线运行速度更快
- hash:每次webpack打包构建时会生成一个唯一的hash值
- 修改配置文件
const { resolve } = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const commonCssLoader = [
MiniCssExtractPlugin.loader,
'css-loader',
{
// 兼容性处理
loader: "postcss-loader",
options: {
postcssOptions: {
ident: "postcss",
plugins: ["postcss-preset-env"],
},
},
},
];
module.exports = {
entry: "./src/js/index.js",
output: {
// 文件资源缓存:js文件名加上10位contenthash值
filename: "js/built.[contenthash:10].js",
path: resolve(__dirname, "build"),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
enforce: 'pre', // 优先执行
loader: "eslint-loader",
options: {
fix: true
}
},
{
oneOf: [
{
test: /\.css$/,
use: [...commonCssLoader],
},
{
test: /\.less$/,
use: [...commonCssLoader,'less-loader'],
},
{
// js兼容性处理
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {version: 3},
targets:{
chrome: '60',
firefox: '50'
}
},
],
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},
{
// 处理图片
test: /\.(jpg|png|gif)$/,
loader: 'url-loader',
options: {
esModule: false,
name: "[hash:10].[ext]",
limit: 8 * 1024,
outputPath: 'imgs'
}
},
{
test: /\.(html)$/,
loader: 'html-withimg-loader',
},
{
exclude: /\.(html|js|less|css|jpg|png|gif)$/,
loader: 'file-loader',
options: {
outputPath: 'media',
name: "[hash:10].[ext]"
}
}
]
}
],
},
plugins: [
// 文件资源缓存:css文件名加上10位contenthash值
new MiniCssExtractPlugin({ filename: "css/built.[contenthash:10].css" }),
new OptimizeCssAssetsWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
})
],
mode: "production",
devtool: 'source-map'
};
5.4 tree shaking
- 去掉无用的代码,使代码的体积更小
- 当(1)必须使用ES6模块化(2)开启production模式 时,自动对代码进行tree shaking
- 作用:减少代码打包体积
- 问题:tree shaking 可能无意把一些副作用文件干掉,比如css,需要在package.json中配置
"sideEffects": "false"
:“false” 所有代码都没有副作用(都可以进行tree shaking)。问题:可能会把css / @babel/pollyfill (副作用)文件干掉,打包后没有css文件"sideEffects": ["*.css", "*.less"]
:把css,less等标记为不要进行 tree shaking 的资源
5.5 code split
- 方式一:单入口 变为 多入口
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 单入口
// entry: './src/js/index.js',
// 多入口,有几个入口就打包成几个chunk
entry: {
main: './src/js/index.js',
test: './src/js/test.js'
},
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
module: {},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
],
mode: 'production'
}
问题:如果多入口中有多个package都引入了node\_modules的依赖,这样会造成node\_modules打包多次
2. 方式二:多入口,加入对公共依赖打包的优化(用的比较少)
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
main: './src/js/index.js',
test: './src/js/test.js'
},
output: {
filename: 'js/[name].[contenthash:10].[ext]',
path: resolve(__dirname, 'build')
},
module: {},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
],
mode: 'production',
// 1. 可以node_modules中代码单独打包一个chunk,最终输出
// 2. 自动分析多入口chunk中有没有公共的文件,如果有会打包成单独一个chunk,不会重复打包多次
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
- 方式三:单入口,加上optimization的方式(保证将node_modules的代码进行分割),其次,在js文件中修改,使其他文件也能够单独打包
- 修改配置文件
const{ resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
module: {},
plugin: [
new HtmlWebpackPlugin({template: './src/index.html'})
],
mode: 'production',
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
- 修改js文件:通过js代码让某个文件单独打包成一个chunk,import动态导入语法,能将某个文件单独打包
// import 返回一个promise对象
// test.js中有个两数相乘的方法 mul(x, y)
/* webpackChunkName: "" */
// 是 Magic Comments(魔术注释法)。Webpack通过增加内联注释来告诉运行时,该有怎样的行为。
// 通过向import中添加注释,我们可以执行诸如命名chunk或选择不同模式之类的操作。
import(/* webpackChunkName: "test" */'./test')
.then(({mul}) => {
// eslint-disable-next-line
console.log(mul(3, 5))
})
.catch(() => {
// eslint-disable-next-line
console.log('文件加载成功');
})
5.6 懒加载和预加载
5.6.1 Lazy loading
- 懒加载:延迟加载,在触发某些条件时才加载文件。也就是在文件需要时再加载
- 实现方法:主要是通过修改js代码实现,将import放在一个异步的回调函数中
- 例子:点击按钮计算一些东西
document.getElementById('btn').onclick(function(){
// import放在异步的回调函数中,点击按钮才会加载test.js
// 这个语法和分割代码的没有什么区别,因此懒加载的前提条件一定是先进行代码分割
import(/* webpackChunkName: 'test' */ .'/test/')
.then(({mul}) => { // 解构赋值,找到mul方法
console.log(mul(4, 5))
})
})
5.6.2 预加载 Prefetch
- 预加载:prefetch会在使用之前,提前加载js文件
- 特点:正常加载可以认为是并行加载(同一时间加载多个文件),而预加载prefetch:等其他资源加载完毕,浏览器空闲了再偷偷加载资源。
- 懒加载是用的时候再加载,如果文件较大,会给用户第一次使用时造成延续的效果;预加载不会阻塞其他资源的加载,而且不会造成延迟的效果,但是兼容性比较差,只能在一些PC端的高版本浏览器中使用,预加载慎用!
- 例子:点击按钮计算一些东西
document.getElementById('btn').onclick(function(){
import(/* webpackChunkName:'test', webpackPrefetch: true */'./test')
.then(({mul}) => {
console.log(mul(2, 4))
})
})
5.7 PWA
- PWA(Progressive Web App):渐进式网络应用开发程序(离线可访问技术)
- PWA有serviceWorker加上Cache方法构成的,作用时帮助我们像APP应用一样,可以离线访问网页,性能也更好。但是PWA由于兼容性问题,普及开来还需要时间,一些大厂在用,比如淘宝。(2021年,貌似淘宝也放弃了PWA技术:))
- 使用插件:workbox --> workbox-webpack-plugin
- 实现
- 修改配置文件
// 在webpack.config.js中 plugins 添加一个插件 new WorkboxWebpackPlugin.GenerateSW({ /* 1 帮助 serviceWorker 快速启动 2 删除旧的 serviceWorker 最终 生成一个 serviceWorker 的配置文件,通过这个serviceWorker文件注册serviceWorker */ clientsClaim: true, skipWaiting: true })
- 修改入口js文件,注册serviceWorker(有兼容性问题,还需要处理兼容性问题)
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(() => { console.log('service worker 注册成功'); }) .catch(() => { console.log('service workder 注册失败'); }); }); }
- 使用
- 注意eslint不认识 window、navigator全局变量。解决:需要修改package.json中eslintConfig配置
"eslintConfig": { "extends": "airbnb-base", "env": { "browser": true // 支持浏览器端全局变量 } }
- sw代码必须运行在服务器上,可以用serve这个库,能帮我快速创建一个服务器
- 安装
npm i serve -g
- 启动
serve -s build
(启动服务器,将build目录下所有资源作为静态资源暴露出去)
- 安装
- 最后再构建:
webpack
5.8 多进程打包
- 使用 thread-loader,将thread-loader放在其他loader上边,就可以开启多进程打包
- 注意:进程开启大概为600ms, 进程通信也有开销,只有工作消耗时间比较长,才需要多进程打包
- 多进程打包一般时对js文件,因为文件会比较多
- 实现
// 比如在js兼容性处理时,使用多进程打包
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 不需要修改thread-loader配置时
// 'thread-loader',
// 需要修改thread-loader配置时
{
loader: 'thread-loader',
options: {
workers: 2 // 设置进程数目
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuildIns: 'usage',
corejs: {version: 3},
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
cacheDirectory: true
}
}
]
}
5.9 externals
-
作用:防止将某些包打包到最后输出的bundle中。比如,我们使用jQuery依赖,我们如果用cdn连接使用jQuery,我们不希望打包jQuery进来
-
修改配置文件webpack.config.js
externals: { // npm包名 -- 页面引入jquery时提供的变量 jquery: 'jQuery' // 拒绝jQuery被打包进来 }
-
忽略打包之后,一定要记得在index.html中将jQuery手动引进来
<script src='https://cdn.bootcdn.net/ajax/libs/jquery/1.9.1/jquery.min.js'></script>
5.10 dll
- 使用dll技术对某些库进行单独打包,比如jquery,React,Vue
- 需要一个 webpack.dll.js 文件(这个文件名可以修改)。以jQuery单独打包为例,单独打包后在dll文件夹中会生成一个manifest.json文件和打包后的jQuery文件。manifest.json提供一个打包后的库和原来的库的映射关系,可以告诉webpack哪些库不需要打包
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
// 最终打包生成的[name] --> jquery
// ['jquery'] --> 要打包的库时jQuery
jquery: ['jquery']
},
output: {
filename: '[name].js',
path: resolve(__dirname, 'dll'),
library: '[name]_[hash:10]', // 打包的库里边向外暴露出去的那内容叫什么名字
},
plugins: [
new webpack.DllPlugin({
// 打包生成一个manifest.json文件,提供和jquery的映射
// 通过这个映射就知道jquery这个库不需要打包,包的名称为 '[name]_[hash:10]'
name: '[name]_[hash:10]', // 映射库的暴露的内容的名称
path: resolve(__dirname, 'dll/manifest.json')
})
],
mode: 'production'
}
- 运行webpack.dll.json文件
- 当运行webpack时,默认查找 webpack.config.js 配置文件,如果需要运行 webpack.dll.js 文件,使用下面的命令:
webpack --config webpack.dll.js
- 当运行webpack时,默认查找 webpack.config.js 配置文件,如果需要运行 webpack.dll.js 文件,使用下面的命令:
- 使用dll打包后,还需要修改webpack.config.js配置
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
// 告诉webpack哪些库不参与打包,同时使用jQuery时的名称也需要改变
new webpack.DllReferencePlugin({
manifest: resolve(__dirname, 'dll/manifest.json')
}),
// 将某个文件打包输出去,并在HTML中自动引入该资源
// 将之前打包的js库引进来
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/jquery.js')
})
],
- dll和externals的区别
- dll是需要打包,只需要打包一次,以后就不需要再打包了
- 如果是使用cdn链接引入,建议使用externals的方式
- 如果是第三方的库,需要打包放在一起,可以使用dll
5.11 性能优化总结
5.11.1 webpack性能优化
- 开发环境性能优化
- 生产环境性能优化
5.11.2 开发环境性能优化
- 优化打包构建速度
- HMR 模块热替换:利用缓存,只需要替换修改的文件
- css文件:style-loader自动做,不需要我们操心!
- js文件:默认不支持,需要修改js文件手动实现
- html文件:不需要做,一般只有一个
- HMR 模块热替换:利用缓存,只需要替换修改的文件
- 优化代码调试
- source-map:提供源代码到构建后代码的映射关系
5.11.3 生产环境性能优化
- 优化打包构建速度(提升开发者的体验)
- oneOf:切记如果一个文件需要两个loader处理,不能都放在oneOf里边
- babel缓存:优化打包构建速度,项目中js代码是最多的,有利于js文件的处理
- cacheDirectory: true
- 让第二次打包构建速度更快
- 多进程打包:应用程序过多过复杂的时候,开启thread-laoder多进程打包,一般是针对babel-loader进行优化
- 优化代码运行性能(提升用户的体验)
- 缓存:文件缓存,将打包输出的css和js文件名字上加上哈希值
- hash
- chunkhash
- contenthash
- tree shaking:优化代码性能,将没有的代码和库去除,让代码体积更小
- 必须使用ES6模块化
- 开启production环境
- package.json设置sideEffects,防止删除了需要的文件(比如样式css代码)
- code split:将bundle拆分成多个js文件,可以并行加载
- 单入口场景
- 通过 optimization 分割 node_modules 代码
- 通过 import 分割特定的 js 代码
- 多入口场景:有几个入口就输出几个bundle,也会加上optimization将公共文件提取成一个,也可以提起node_modules代码
- 单入口场景
- lazy loading 和 prefetch
- lazy loading:触发时候再加载再执行,文件较大时会卡顿
- prefecth:闲时预先加载,但是兼容性太差(可以使用https://caniuse.com/网站查看是否可以使用)
- PWA:serviceWorker和cache方法组成
- externals:让某些库不打包
- 声明哪些库不打包
- 用script标签将cdn库引进来
- dll:让某些库打包
- 单独将一下库先打包好,后边用不需要再打包
- dll可以和code split结合使用,code split借助optimization将node_modules单独打包,然后通过dll将node_modules第三方库分别打包,再一个个引入
- 缓存:文件缓存,将打包输出的css和js文件名字上加上哈希值