webpack 性能优化
- 开发环境性能优化
- 生产环境性能优化
开发环境性能优化
- 优化打包构建速度
- HMR 热模块替换
- 构建时,只有一个模块发生变化,那么只会重新构建这一个模块,
而其他模块都会从缓存中读取,不再重新构建 - 针对 css/js/html 文件,html文件一般只有一个,不需要此操作;而对于css文件,style-loader是默认可以进行此操作,
js 默认不支持,需要自己进行配置
- 构建时,只有一个模块发生变化,那么只会重新构建这一个模块,
- HMR 热模块替换
- 优化代码调试
- source-map 提供源代码到构建后代码映射的技术
- 要清除开发环境和生产环境的推荐使用
- 开发环境推荐: eval-source-map / eval-cheap-module-source-map
- 生产环境推荐: source-map / cheap-module-source-map
- source-map 提供源代码到构建后代码映射的技术
生产环境优化
-
优化打包构建速度
- oneOf
- 找到处理文件对应的loader时就停止继续查询,而不是每一个loader全部都过一遍
- 注意: 当一个文件需要两个或者多个loader处理时,将其中一个放在oneOf里面,其余放在外面
- babel 缓存
- 第一次构建时,会缓存babel构建的结果,再次构建时,直接从缓存中读取
- 通过babel-loader优化对js文件的处理
- oneOf
-
优化代码运行的性能
- 缓存 (进行强制缓存时,若文件发生改变时,为了识别到变化与否而进行的操作)
- hash 无论文件内容是否变化,当重构建时,都会重置哈希值,导致重新构建没有发生变化的文件
- chunkhash 来自同一个chunk(打包时来自于同一个入口的文件共享同一个哈希值),样式文件和js文件哈希值相同,
一旦样式文件发生变化而js文件不变,js文件哈希值也要发生变化;同理,只改变js文件而不改变样式文件,样式文件哈希值也变 - contenthash 根据文件内容生成哈希值,文件内容不同,哈希值一定不同
- tree shaking
- 去除掉应用程序中我们没有使用到的代码 (满足两个前提,自动开启tree shaking)
- code split 将一个大的js文件在构建时拆分成多个js文件,然后可以并行加载,节约时间
- 单入口文件
- 多入口文件
- 缓存 (进行强制缓存时,若文件发生改变时,为了识别到变化与否而进行的操作)
HMR : hot module replacement 热模块替换/模块热替换
- 作用: 一个模块发生变化,只会重新打包这一个模块(而不是重新打包所有),提升构建速度
- 样式文件: 可以使用HMR功能:因为style-loader内部实现了
- js文件: 默认不能使用HMR功能
- 解决: 修改js代码,添加支持HMR功能的代码
- 一旦module.hot为true,说明开启了HMR功能 —> 让HMR功能功能生效
if(module.hot){
// 此方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建,会执行后面的回到函数
module.hot.accept('./print.js',function () {
print();
})
}
- 注意: HMR功能对js的处理,只能处理非入口js文件
- html文件: 默认不能使用HMR功能,同时会导致问题:html文件不能热更新了(不用做HMR功能)
- 解决: 修改entry入口,将html文件引入
entry: ['./src/js/index.js','./src/index.html'],
source-map:
一种提供源代码到构建后代码映射关系的技术(如果构建后代码出错了,通过映射关系可以追踪源代码错误)
- [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
devtool:'eval-source-map',
inline-source-map 内联
- 只生成了一个 source-map
- 错误代码准确信息 和 源代码的错误位置
hidden-source-map 外部
- 错误代码 和 错误原因, 但是没有错误位置
- 不能提示源代码错误,只能提示到构建后代码的错误位置
eval-source-map 内联
- 每一个文件都生成了对应的 source-map,在built.js内部生成
nosourcrs-source-map 外部
- 错误代码准确信息 但是没有任何源代码的错误信息
cheap-source-map
- 错误代码的准确信息 和 源代码的错误位置
- 只能精确到行,但不能精确到列
cheap-module-source-map
- 错误代码准确信息 和 源代码的错误位置
- module会将loader的source map 加入
内联 和 外部的区别:
-
- 外部生成了文件,内联没有 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
正常来说,一个文件会被所有的loader过滤处理一遍,如果我有100个loader配置,那么我一个文件就要被100个loader匹配,而使用oneOf后,而如果放在oneOf中的loader规则有一个匹配到了,oneOf中的其他规则就不会再对这文件进行匹配
注意:oneOf中不能有两个loader规则配置处理同一种文件,否则只能生效一个 例如:对于js进行eslint检测后再进行babel转换
解决:将eslint抽出到外部,然后优先执行,这样在外部检测完后oneOf内部配置就会再进行检测匹配
以下loader只会匹配一个
注意: 不能有两个配置处理同一个文件
oneOf:[
{
test:/\.css$/,
use:[
// 此loader会将css代码以标签的形式整合进js代码中
// 'style-loader',
...commonCssLoader
]
},
{
test:/\.less$/,
use:[
...commonCssLoader,
'less-loader'
]
},
/**
* 正常来讲,一个文件只能被一个loader处理。
* 当一个文件要被多个loader处理时,那么一定要指定loader的先后顺序
* 先执行 eslint,再执行babel
*/
// 在package.json中的eslintConfig ---> airbnb规则
// 对js做兼容性处理
{
test:/\.js$/,
exclude:/node_module/,
loader:'babel-loader',
options:{
presets:[
// 转译新的es6语法
'@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,
}
},
{
tset:/\.html$/,
loader:'html-loader'
},
{
exclude:/\.(js|html|less|jpg|png|gif|css)/,
loader: 'file-loader',
options: {
outputPath: 'media',
}
}
]
缓存
babel缓存
cacheDirectory: true
让第二次打包构建速度更快
文件资源缓存
hash: 每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。
(可能我却只改动一个文件)如果重新打包,会导致所有缓存失效。
chunkhash:代码块缓存
根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的
因为css是在js中被引入的,所以同属于一个chunk
chunk:从一个入口文件引入的其他依赖和其他文件,都属于同一个chunk文件
contenthash
根据文件的内容生成hash值。不同文件hash值一定不一样
让代码上线运行缓存更好使用
treeShaking
指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能来帮我们实现。
官方有标准的说法:Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)
在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 Tree-Shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
在production 模式下不用在webpack.config.js中配置
前提: 1. 必须使用ES6模块化 2. 开启production环境
满足这两个前提会自动开启 tree shaking
作用: 减少代码体积
在package.json中配置
"sideEffects": false 即所有代码都没有副作用(都可以进行tree shaking)
问题: 可能会将css/ @babel/ployfill (副作用)文件干掉
"sideEffects":["*.css"] 将css资源标记为不会进行tree shaking的资源
代码分割
将打包输出的一个 chunk 分割成多个 chunk,加载时候可以并行加载等等,加快加载速度,还可实现按需加载等等
主要是对js代码进行分割
方法1 根据入口文件进行代码分割
// 单入口 一般对应单页面应用
// entry: './src/js/index.js',
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: './src/js/index.js',
test: './src/js/test.js'
},
output: {
// [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
方法2 optimization配置项进行优化
- 对于单入口文件,可以将node_modules中的代码单独打包成一个chunk,最终输出
- 对于多入口文件,会自动分析多入口chunk中,有没有公共的文件.如果有会将相同的公共文件打包成一个单独的chunk
optimization:{
splitChunks:{
chunks:'all'
}
},
方法三 通过js代码,让某个文件被单独打包成一个chunk
// ES10 的 import语法
//通过注释,可以让js生成的打包文件带上这个名字
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul, count }) => {
// 文件加载成功~
// eslint-disable-next-line
console.log(mul(2, 5));
})
.catch(() => {
// eslint-disable-next-line
console.log('文件加载失败~');
});
// eslint-disable-next-line
console.log(sum(1, 2, 3, 4));
懒加载 (lazy loading) 和预加载
应用场景:当我们模块很多时,导入的js太多,或者说有的js只有使用的时候才有用,而我一开始便加载,就可能造成一些不必要的性能浪费
1、懒加载:当文件需要使用时才加载
可能的问题:当用户第一次使用时,如果js文件过大,可能造成加载时间过长(有延迟),但是第二次就不会了,因为懒加载第二次是从缓存中读取文件
2、预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载
正常加载可以认为时并行加载(同一时间加载多个文件,但是同一时间有上限)
就例如下面例子,有预加载的代码运行效果,是页面刷新后,但是还未进行使用时,该文件其实已经加载好了
注意:预加载虽然性能很不错,但是需要浏览器版本较高,兼容性较差,慎用预加载
console.log('index.js文件被加载了~');
// import { mul } from './test';
//懒加载
document.getElementById('btn').onclick = function() {
//懒加载其实也是需要前面Ⅵ代码分割功能,将我的需要加载的文件打包成单独文件
import(/* webpackChunkName: 'test'*/'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
//预加载
//在注释参数上添加 webpackPrefetch: true
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
PWA (离线访问)
PWA: 渐进式网络开发应用程序(离线可访问) workbox -->下载依赖: workbox-webpack-plugin
1、在配置中使用该插件 :① 帮助serviceworker快速启动 ② 删除旧的 serviceworker
2、在入口文件js中添加代码
3、eslint不认识 window、navigator全局变量
解决:需要修改package.json中eslintConfig配置
4、代码必须运行在服务器上才有效果
① node.js
② npm i serve -g -->serve -s build 启动服务器,将build目录下所有资源作为静态资源暴露出去
webpack.config.js新增配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
plugins: [
new WorkboxWebpackPlugin.GenerateSW({
/*生成一个 serviceworker 配置文件~*/
//1. 帮助serviceworker快速启动
clientsClaim: true,
//2. 删除旧的 serviceworker
skipWaiting: true
})
],
入口文件js -->index.js
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}
package.json新增配置
"eslintConfig": {
"extends": "airbnb-base",
"env": {
"browser": true //开启为eslint支持浏览器端的bian'l,比如 window
}
},
多线程打包
1、下载thread-loader依赖
2、使用loader: 'thread-loader’开启多线程打包
注意点:进程启动大约为600ms,进程通信也有开销,只有工作消耗时间较长,才需要多进程打包 比如:babel转换可以使用多线程
const { resolve } = require('path');
module.exports = {
module: {
rules: [
oneOf: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/*
开启多进程打包。
进程启动大概为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
}
}
]
}
]
}
]
}
};
externals
当你使用外部引入代码时:如CDN引入,不想他将我引入的模块也打包,就需要添加这个配置
即:声明哪些库是不进行打包的
–>externals: {}
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',
externals: {
// 拒绝jQuery被打包进来
jquery: 'jQuery'
}
};
dll
使用dll技术,对某些库(第三方库:jquery、react、vue…)进行单独打包
作用:如果不是cdn引入,而是使用第三方库,想要打包后暴露出去,使用该方法
1、首先你需要写一个新的配置文件,因为使用dll技术,所以命名为webpack.dll.js
当你运行 webpack 时,默认查找 webpack.config.js 配置文件 需求:需要先运行 webpack.dll.js 文件
–> webpack --config webpack.dll.js 在这个文件中进行对某些库的单独打包
2、在webpack.config.js中,需要告诉webpack哪些库不需要再次打包(即在dll.js中打包后生成的文件)
3、这里需要使用到add-asset-html-webpack-plugin与webpack插件
4、运行webpack.dll.js对第三方库进行单独打包后,除非你要加新的库,不然不用再重新打包这个,直接webpack打包其他的即可
webpack.dll.js配置文件
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
// 最终打包生成的[name] --> jquery
// ['jquery'] --> 要打包的库是jquery
jquery: ['jquery'],
// react:['react','react-dom' ]
},
output: {
filename: '[name].js',
path: resolve(__dirname, 'dll'),
library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins: [
// 打包生成一个 manifest.json --> 提供和jquery映射
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};
webpack.config.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中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/jquery.js')
})
],
mode: 'production'
};