对于开发环境而言,我们主要优化打包构建速度
和优化代码调试
。
开发环境性能优化
对于生产环境而言,我们主要优化打包构建速度
和优化代码运行的性能
。
生产环境性能优化
HMR
HMR(Hot Module Replacement)是一种前端开发中常用的技术,用于在开发过程中实现模块的热替换,从而实现实时预览和快速开发的效果。HMR 可以使开发者在不刷新整个页面的情况下,实时更新修改的模块,以提高开发效率。
HMR 的基本原理是在应用程序运行过程中,将新的模块代码替换掉旧的模块代码,同时保持应用程序的状态和数据。通过使用热更新中间件和热模块替换插件,开发者可以在代码发生变化时,将新的模块代码注入到运行中的应用程序中,从而实现模块级别的热替换。
对于样式文件,当我们使用style-loader
时,我们可以直接使用HMR功能。对于html文件,因为只有一个,所以默认我们不使用HMR功能。HMR 主要用于处理 JavaScript 模块的热更新。
开启HMR默认功能时,我们只需配置devServer就可以了。
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
hot: true
}
但是这个时候,一旦有任意模块中的js代码发生变化都会重新加载。为了只加载发生变化的模块,我们在入口文件针对其他子模块做判断
if (module.hot) {
// 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
module.hot.accept()
}
module.hot.accept()
是 HMR 的 API,用于接受新模块的更新。当某个模块发生变化时,会触发 HMR,然后执行 module.hot.accept()
来接受更新,并将新的模块替换掉旧的模块。
但是生产环境不需要开启热模块,所以针对以上代码我们要稍作修改。
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
module.hot.accept();
}
}
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. 外部生成了文件,内联没有 2. 内联构建速度更快
按打包速度排序:
- eval-souce-map
- inline-souce-map
- cheap-souce-map
按调试友好度:
- souce-map
- cheap-module-souce-map
- cheap-souce-map
对于开发环境我们追求速度快,调试更友好,我们建议: eval-source-map
(react和vue的脚手架的选择)
对于生产环境我们要考虑构建包的体积大小,源代码要不要隐藏,调试要不要更友好?
- 内联会让代码体积变大,所以在生产环境不用内联
- 如果要全部隐藏,nosources-source-map
- 只隐藏源代码,会提示构建后代码错误信息,hidden-source-map
- 调试友好,source-map
- 速度快一点:cheap-module-souce-map
最终我们建议使用:source-map
oneOf
oneOf
是 rules
数组的一个特殊配置项,用于处理特定规则的加载器。它允许我们对不同类型的文件应用不同的加载器,并且只使用第一个匹配的规则,而不会继续向下应用其他规则。
换句话说,rules中对应的每个loader的test正则匹配会去读取所有文件,而用oneOf包裹之后,之前匹配到的文件类型不再进行重复匹配。
rules: [
{
// 在package.json中eslintConfig --> airbnb
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 缓存
在babel-loader中配置cacheDirectory: true,可以让第二次打包构建速度更快
{
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值来命名,当名称发生变化时,浏览器会重新从服务器加载资源。这样做是让代码上线运行缓存更好使用。hash有以下3种。
hash
每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
chunkhash
根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样。(同一个入口文件,构建后同属于一个chunk)
问题: js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk
contenthash
根据文件的内容生成hash值。不同文件hash值一定不一样
output: {
filename: 'js/built.[contenthash].js',
path: resolve(__dirname, 'build')
},
new MiniCssExtractPlugin({
filename: 'css/built.[contenthash].css'
}),
tree shaking
它的作用是去除无用代码,减少代码体积。
如何使用:
- 必须使用ES6模块化
- mode 配置 为production环境
由于webpack版本的差异,有的版本会把样式文件和配置文件browserslist当作无用代码。针对这种情况,我们可以在package.json
中配置sideEffects
"sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)
"sideEffects": ["*.css", "*.less"]
code split
多入口
在entry中配置多入口,webpack会根据不同入口打包生成不同的boundle。
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: './src/js/index.js',
test: './src/js/test.js'
},
output: {
// [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
同时在output中对filename输出使用[name] 占位符,这样输出的boundle名会跟入口名保持一致。
optimization
optimization
是 webpack 的配置项之一,用于对打包输出进行优化。它提供了多个配置选项,可以根据项目的需求进行配置。
minimize
:是否启用代码压缩,默认为true
。设置为false
可以禁用代码压缩。minimizer
:指定用于代码压缩的插件。可以配置多个压缩插件,例如TerserWebpackPlugin
、OptimizeCssAssetsWebpackPlugin
等。splitChunks
:用于拆分代码块的配置。可以将共享的模块拆分成单独的文件,提高缓存利用率和加载速度。可以配置cacheGroups
进一步细分代码块的拆分规则。runtimeChunk
:是否将 webpack 的运行时代码提取为单独的文件,默认为false
。设置为true
可以将运行时代码提取为独立的文件,可以避免每次构建时运行时代码的变化导致缓存失效。moduleIds
:用于生成模块标识符的方式,默认为'natural'
。可以配置为'named'
、'hashed'
等方式,控制生成的模块标识符的可读性和唯一性。chunkIds
:用于生成代码块标识符的方式,默认为'natural'
。可以配置为'named'
、'hashed'
等方式,控制生成的代码块标识符的可读性和唯一性。
因此我们可以使用splitChunks.chunks
来进行代码分割。它有如下可选值:
async
:将异步加载的模块拆分为单独的文件。initial
:将入口模块中的公共模块拆分为单独的文件。all
:将异步和入口模块中的公共模块都拆分为单独的文件。
通常情况下,我们配置为all
/* 代码省略 */
optimization: {
splitChunks: {
chunks: 'all'
}
},
mode: 'production'
import() 动态导入语法
在入口文件中,对模块使用import()函数导入,能将某个文件单独打包
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul, count }) => {
// 文件加载成功~
// eslint-disable-next-line
console.log(mul(2, 5));
})
.catch(() => {
// eslint-disable-next-line
console.log('文件加载失败~');
});
懒加载 和 预加载
懒加载就是当文件 需要使用时才加载。
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test'/'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
预加载等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
它们都是使用了import()语法
PWA
渐进式网络开发应用程序(离线可访问)。
首先我们要使用workbox,在webpack中配置workbox-webpack-plugin
plugins: [
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker
生成一个 serviceworker 配置文件~
*/
clientsClaim: true,
skipWaiting: true
})
],
然后,我们在入口文件中使用serviceWorker对象
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}
多进程打包
在我们前端项目中,最多的就是js代码,而在js代码中最多的就是Babel的兼容性处理,因此当项目越来越大时,我们需要对babel采用多进程打包。多进程打包,我们使用thread-loader
{
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
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。
以jQuery为例:
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
同时我们在index.html 中引入cdn资源
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
DLL
DLL(Dynamic Link Library)是一种动态链接库的技术,用于提高代码的复用性和构建速度。它可以将一些稳定不变的第三方库或模块提前打包成独立的 DLL 文件,然后在构建过程中直接引用这些 DLL 文件,而不需要重复构建这些库或模块。这样可以减少构建时间,并提高开发和构建的效率。
使用 DLL 技术的主要步骤包括:
- 创建一个用于生成 DLL 文件的配置文件,通常命名为 webpack.dll.config.js。该配置文件中定义了需要打包为 DLL 的库或模块,以及输出的 DLL 文件名和路径。
以 jQuery 为例 编写dll配置文件:
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]' // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins: [
// 打包生成一个 manifest.json --> 提供和jquery映射
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};
以上代码我们使用webpack提供的DllPlugin
打包生成一个 manifest.json文件。
- 运行 webpack 命令,并指定使用上述的配置文件:webpack –config webpack.dll.config.js。这将生成 DLL 文件。
- 在常规的构建过程中,通过引入 DLL 文件,直接使用其中的模块或库,而不需要重新构建。因此我们要修改webpack的配置文件
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'
};
以上代码中,我们使用webpack提供的DllReferencePlugin
读取dll/manifest.json文件,同时告诉webpack哪些库不参与打包。然后我们又使用AddAssetHtmlWebpackPlugin插件把dll/jquery.js在html中自动引入。
使用 DLL 技术可以有效减少构建时间,尤其是对于那些稳定不变的第三方库或模块。它可以在开发过程中提高开发效率,并在构建时加快打包速度。