一. 性能优化介绍
开发环境性能优化
- 优化打包构建速度
- 优化代码调试
生产环境性能优化
- 优化打包构建速度
- 优化代码运行的性能
二. HMR
HMR: hot module replacement 热模块替换 / 模块热替换
作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),极大提升构建速度。
- 样式文件:可以使用HMR功能:因为style-loader内部实现了~
js
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 当修改了webpack配置,新配置要想生效,必须重新webpack服务
hot: true
}
- js文件:默认不能使用HMR功能 --> 需要修改js代码,添加支持HMR功能的代码
注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。
js
// index.js
import print from './print';
if (module.hot) {
// 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
module.hot.accept('./print.js', function() {
// 方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建。
// 会执行后面的回调函数
print();
});
}
- html文件: 默认不能使用HMR功能.同时会导致问题:html文件不能热更新了~ (不用做HMR功能)
解决:修改entry入口,将html文件引入
js
entry: ['./src/js/index.js', './src/index.html'],
三. Source-map
source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)。
使用
js
devtool: 'eval-source-map'
js
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
种类
js
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加入
内联 和 外部的区别:
-
外部生成了文件,内联没有
-
内联构建速度更快
使用选择
开发环境:速度快,调试更友好
速度快(eval>inline>cheap>...)
js
eval-cheap-souce-map
eval-source-map
调试更友好
js
souce-map
cheap-module-souce-map
cheap-souce-map
--> eval-source-map / eval-cheap-module-souce-map
生产环境:源代码要不要隐藏? 调试要不要更友好
内联会让代码体积变大,所以在生产环境不用内联
js
nosources-source-map 全部隐藏
hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
--> source-map / cheap-module-souce-map
四. oneOf
oneOf可以使loader只匹配一次,避免重复执行。
使用oneOf数组将loader包裹起来,重复的要提取出来放到外面。
js
module: {
rules: [
// 在使用oneOf时与babel-loader重复,提取出来
{
test: /.js$/,
exclude: /node_modules/,
// 优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// 以下loader只会匹配一个
// 注意:不能有两个配置处理同一种类型文件
oneOf: [
{
test: /.css$/,
use: [...commonCssLoader]
},
{
test: /.less$/,
use: [...commonCssLoader, 'less-loader']
},
/*
正常来讲,一个文件只能被一个loader处理。
当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
先执行eslint 在执行babel
*/
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
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]',
outputPath: 'imgs',
esModule: false
}
},
{
test: /.html$/,
loader: 'html-loader'
},
{
exclude: /.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
}
五. 缓存
babel缓存
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
}
},
让第二次打包构建速度更快
文件资源缓存
当在服务器端设置了文件的缓存过期时间,那么在此时间内都只会读取缓存的内容,不会向服务器请求资源。那么当上线后的项目更改内容后,在缓存时间内就无法更新内容。
hash
: 每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。
如果重新打包,会导致所有缓存失效。(可是只改动一个文件,所有的文件缓存都失效了)
js
// js文件
output: {
filename: 'js/built.[hash:10].js',
path: resolve(__dirname, 'build')
},
// css文件
new MiniCssExtractPlugin({
filename: 'css/built.[hash:10].css'
}),
chunkhash
:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的
因为css是在js中被引入的,所以同属于一个chunk,所以缓存同时失效的问题依然存在。
js
// js文件
output: {
filename: 'js/built.[chunkhash:10].js',
path: resolve(__dirname, 'build')
},
// css文件
new MiniCssExtractPlugin({
filename: 'css/built.[chunkhash:10].css'
}),
contenthash
: 根据文件的内容生成hash值。不同文件hash值一定不一样 (推荐)
解决了所有文件或者来源相同的文件缓存同时失效的问题。
js
// js文件
output: {
filename: 'js/built.[contenthash:10].js',
path: resolve(__dirname, 'build')
},
// css文件
new MiniCssExtractPlugin({
filename: 'css/built.[contenthash:10].css'
}),
作用:让代码上线运行缓存更好使用
js
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');
process.env.NODE_ENV = 'production';
// 复用loader
const commonCssLoader = [
MiniCssExtractPlugin.loader,
'css-loader',
{
// 还需要在package.json中定义browserslist
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [require('postcss-preset-env')()]
}
}
];
module.exports = {
entry: './src/js/index.js',
output: {
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']
},
{
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: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
outputPath: 'imgs',
esModule: false
}
},
{
test: /.html$/,
loader: 'html-loader'
},
{
exclude: /.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/built.[contenthash:10].css'
}),
new OptimizeCssAssetsWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
})
],
mode: 'production',
devtool: 'source-map'
};
六. Tree shking
什么是tree shking
tree shaking首先是由rollup的作者提出的,它是DCE(dead code elimination)的一个实现,通过tree shaking的分析,可以使你代码里没有使用的代码全部删除。实际情况中,在 webpack 项目中,入口文件有很多依赖的模块,但是有时候虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。然而它又区别于普通的dec,这里作者有一个比喻很形象:
imagine that you made cakes by throwing whole eggs into the mixing bowl and smashing them up, instead of cracking them open and pouring the contents out. Once the cake comes out of the oven, you remove the fragments of eggshell, except that's quite tricky so most of the eggshell gets left in there.
简单翻译一下就是,如果将dec比作制作蛋糕,传统的dec做法就是,将整个鸡蛋丢进碗里搅拌,然后放进烤箱,烤完之后从做好的蛋糕里,找到不需要的蛋壳扔掉,而tree shaking是将鸡蛋打破把蛋黄等有用的东西丢进碗里搅拌,最后直接做出蛋糕。
为什么需要tree shaking
主要还是为了减少页面的加载时间,将无用的代码删除,减少js包的大小,从而减少用户等待的时间,使用户不因为漫长的等待而离开。
那为什么已经有了dec,还要做tree shaking呢,根据作者的意思是,由于js静态语法分析的局限性,从已有代码里去删除代码不如去寻找真正使用的代码来的好。
作用:去除无用代码,减少代码体积。
开启前提
前提:1. 必须使用ES6模块化 2. 开启production环境
js
mode: 'production';
在package.json中配置
js
"sideEffects": false
所有代码都没有副作用(都可以进行tree shaking)
问题:可能会把css / @babel/polyfill (副作用)文件干掉
例如:避免css文件被删除掉
js
"sideEffects": ["*.css", "*.less"]
七. Code split
方式一(配置optimization方式)
单文件使用optimization 打包只能node_modules等第三方库中代码单独打包一个chunk最终输出。
问题:无法单独打包自定义的js文件。
多入口文件使用optimization 打包可以将自定义js代码文件也单独打包。(多入口使用较少)
自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk。
js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { resolve } = require('path');
module.exports = {
// 单入口
// entry: './src/index',
// 多入口
entry: {
index: './src/index',
test: './src/test'
},
output: {
// 输出文件名称需要改为动态 [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
module: {
},
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all'
}
},
mode: 'production'
}
方式二(在js代码中使用import方式)
js
// 单入口
entry: './src/index',
在配置optimization 的基础上,在js文件引入中:
js
// 通过js代码,让某个文件被单独打包成一个chunk
// import动态导入语法:能将某个文件单独打包
// /* webpackChunkName: 'test' */ 为更改文件名
import(/* webpackChunkName: 'test' */'./test')
.then(({num})=>{
// 文件加载成功!
console.log(num(4,2))
})
.catch(()=>{
console.log('文件加载失败')
})
打包文件:
文件打包后目录:
八. 预加载与懒加载
正常加载可以认为是并行加载(同一时间加载多个文件) 。
懒加载:当文件需要使用时才加载。
缺点:当加载文件较大时,用户触发后再加载可能会有卡顿
js
// 懒加载
// webpackChunkName: 'test'更改打包后文件名
// import { mul } from './test';
// 点击按钮后才会加载test文件
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test' */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
预加载 prefetch:会在使用之前,提前加载js文件 ,等其他资源加载完毕,浏览器空闲了,再偷偷加载资源。
缺点:兼容性
js
// 预加载
// webpackPrefetch: true
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
九. PWA
PWA: 渐进式网络开发应用程序(离线可访问)
- 安装插件
js
npm i workbox-webpack-plugin -D
- 在webpack.config.js中配置
js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
const { resolve } = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'build')
},
module: {
rules: []
},
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 引入 WorkboxWebpackPlugin
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker
生成一个 serviceworker 配置文件~
*/
clientsClaim: true,
skipWaiting: true
})
],
mode: 'production'
}
- 在入口文件中注册
js
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}
问题一
eslint不认识 window、navigator全局变量(实际demo中并没有碰到)
解决:需要修改package.json中eslintConfig配置
js
"env": {
"browser": true // 支持浏览器端全局变量
}
问题二
sw代码必须运行在服务器上
处理方式一:使用nodejs搭建服务器
处理方式二:
js
// 使用serve包自动搭建服务器
npm i serve -g
// 启动服务器,将build目录下所有资源作为静态资源暴露出去
// build为打包后文件名
serve -s build
此时请求的文件资源为service worker中:
十. 多进程打包
- 安装插件
js
npm i thread-loader -D
- 使用
js
{
test: /.js$/,
exclude: /node_modules/,
use: [
/*
开启多进程打包。
进程启动大概为600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
// 对babel进行多进程打包
{
loader: 'thread-loader',
options: {
// 配置两个进程
workers: 2
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
cacheDirectory: true
}
}
]
}
只有工作消耗时间比较长,才需要多进程打包,耗时较短的任务开启多进程打包后会比之前所用的时间更长。
十一. externals
排除不想被打包的文件
js
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'build')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
mode: 'production',
// 排除jquery,避免第三方包被打包
externals: {
// 拒绝jQuery被打包进来
jquery: 'jQuery'
}
};
但是要在index.html中指定jquery的远程cdn链接
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack</title>
</head>
<body>
<h1 id="title">hello html</h1>
// 引入cdn链接
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</body>
</html>
十二. Dll
使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
当你运行 webpack 时,默认查找 webpack.config.js 配置文件。
- 创建webpack.dll.js文件(文件名可以随意)
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
jquery: ['jquery'],
},
// 生成jquery文件
output: {
filename: '[name].js',
path: resolve(__dirname, 'dll'),
library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
},
// 生成json与jquery的映射文件
plugins: [
// 打包生成一个 manifest.json --> 提供和jquery映射
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};
- 在项目目录下运行:
js
webpack --config webpack.dll.js
会生成一个dll文件,此文件为独立生成的jquery文件和json映射文件
- 安装插件
js
npm i add-asset-html-webpack-plugin -D
- webpack.config.js中的文件配置
js
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'build')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
// 使第三方包不参与打包的配置
new webpack.DllReferencePlugin({
manifest: resolve(__dirname, 'dll/manifest.json')
}),
// 将某个文件打包输出去,并在html中自动引入该资源
// 自动引入执行webpack.dll.js文件生成的目录文件
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/jquery.js'),
publicPath: ''
})
],
mode: 'production'
};
重新执行webpack打包后,无需手动引入jquery文件,在压缩后的index.html文件中会自动引入。