本文主要总结经常用到的一些代码性能优化、减小代码体积、提升webpack打包构建速度 等内容的方法。具体的实现可参考webpack官网查看相关示例。
注:如果读者还未接触过webpack,请先了解webpack的基本使用。
正文:
SourceMap ---- 提升开发体验
SourceMap源代码映射,是一个用来生成源代码与构建后代码一一映射的文件的方案。
使用webpack打包之后会生成一个与打包文件对应的.map
文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后的代码报错时,其会通过.map
文件将构建后代码中出错的位置映射到源代码出错的位置,从而让浏览器的报错提示的是源代码文件报错的位置信息,帮助开发人员快速定位。
解决方案: 可以通过设置devtool来控制如何生成source map。开发模式下我们一般使用cheap-module-source-map
,优点:打包编译速度快,只包含行映射,缺点:没有列映射。生产模式下使用source-map
,优点:包含行和列的映射,缺点:打包编译速度慢。
javascript
// webpack.dev.js
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map'
}
// webpack.prod.js
module.exports = {
mode: 'production',
devtool: 'source-map'
}
提升构建打包速度
1. HotModuleReplacement(HMR)模块热替换
在webpack5中,热更新是webpack默认开启的。开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而实现局部更新,而不是刷新整个页面。
javascript
// webpack.config.js
module.exports = {
devServer: {
hot: true
}
}
2. OneOf 规则数组 只使用第一个匹配规则
当在webpack配置文件中写了很多处理不同资源文件的loader
时,资源文件会遍历所有loader
进行解析处理,当使用了oneOf
规则之后,资源文件一旦被某个loader
处理了,就不会继续往下遍历,从而使打包速度更快。
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
oneOf: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s[sc]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.styl$/,
use: ['style-loader', 'css-loader', 'stylus-loader']
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10KB的图片转base64
maxSize: 10 * 1024
}
},
generator: {
filename: 'static/img/[hash:10][ext][query]'
}
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:10][ext][query]'
}
},
{
test: /.\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
]
}
}
3. Include/Exclude
引入或排除某些文件,处理的文件更少,速度更快。例如开发时我们用到了第三方的库或者插件,那么这些文件是不需要编译就可以使用的,所以可以排除(exclude)对这些文件的处理。或者只处理(include)某个文件夹下面的源代码。二者选其一使用。
javascript
// webpack.config.js
const path = require('path')
const ESlintPlugin= require('eslint-webpack-plugin')
module.exports = {
module: {
rules: [
{
test: /.\.js$/,
// 排除node_modules下的文件 不作处理
exclude: /node_modules/,
loader: 'babel-loader'
},
// 或者
{
test: /.\.js$/,
// 只处理src下的内容 其他文件不处理
include: path.resolve(__dirname, './src'),
loader: 'babel-loader'
},
]
},
plugins: [
new ESlintPlugin([
context: path.resolve(__dirname, './src'),
exclude: /node_modules/, // 默认值
//或者
include: path.resolve(__dirname, './src'),
])
]
}
4. Cache 缓存
可以对eslint和babel处理的结果进行缓存,让后续打包速度更快。
javascript
// webpack.config.js
const path = require('path')
const ESlintPlugin= require('eslint-webpack-plugin')
module.exports = {
module: {
rules: [
{
test: /.\.js$/,
// 排除node_modules下的文件 不作处理
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel缓存
cacheCompression: false // 关闭缓存文件压缩
}
},
]
},
plugins: [
new ESlintPlugin([
context: path.resolve(__dirname, './src'),
exclude: /node_modules/, // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, './node_modules/.cache/eslintcache') // 定义缓存位置
])
]
}
5. Thead 多进程打包
当项目非常庞大的时候,打包速度越来越慢,主要是对js文件进行检查(eslint)、编译(babel)、压缩(terser),要提升运行速度可以开启多进程同时处理js文件。由于进程启动通信都是有开销的,所以只有在代码比较多的时候处理才有效果。
javascript
npm i thread-loader --save-dev
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
在 worker 池中运行的 loader 是受到限制的。例如:
- 这些 loader 不能生成新的文件。
- 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
- 这些 loader 无法获取 webpack 的配置。
每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
javascript
// webpack.config.js
const os = require('os')
const ESlintPlugin= require('eslint-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin') // 内置插件
const threads = os.cups().length // 获取CPU核数
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
{
loader: "thread-loader",
options: {
works: threads
}
},
{
loader: "babel-loader"
}
],
},
],
},
plugins: [
new ESlintPlugin([
context: path.resolve(__dirname, './src'),
exclude: /node_modules/, // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, './node_modules/.cache/eslintcache'), // 定义缓存位置
threads, // 开启多进程和设置数量
]),
new TerserPlugin({ // 代码压缩
parallel: threads //开启多进程和设置数量
})
],
// 压缩插件 第二种写法
optimization: {
minimizer: [
new CssMinimizerPlugin(), // css文件压缩
new TerserPlugin({ // js代码压缩
parallel: threads //开启多进程和设置数量
})
]
}
};
减小代码体积
1. Tree Shaking
当我们编写了很多工具函数或者引入了第三方库,可能在实际开发中只应用了其中一部分,那么在打包时这些未用到的代码就无须进行打包。Tree Shaking就帮我们做了这件事情。它可以移除JS上下文中的死代码,且其语法依赖于ESM,不支持CommonJS。
在Webpack中已经默认开启了此配置,所以开发者无需再进行配置。
2. @babel/plugin-transform-runtime
此插件可以对babel进行处理,让辅助代码单独生成到一个文件中,引入到编译后的文件中,而不是每个文件都生成辅助代码,从而减小打包后的体积。
javascript
npm i @babel/plugin-transform-runtime -D
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.\.js$/,
// 排除node_modules下的文件 不作处理
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ['@babel/plugin-transform-runtime'] // 减小代码体积
}
},
]
}
}
3. Image Minimizer 图片压缩
当项目中使用了很多本地图片,那么可以对图片进行压缩,减小图片体积,从而加快请求速度。如果项目中使用的是在线链接的图片,那么就不需要进行配置了。
javascript
npm i image-minimizer-webpack-plugin imagemin -D
安装完上面两个依赖包之后还需要下载另外两种压缩方式的包,读者选择性下载。
一是无损压缩:
javascript
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
二是有损压缩:
javascript
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
javascript
// webpack.config.js
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const { extendDefaultPlugins } = require('svgo');
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset',
},
],
},
plugins: [
new ImageMinimizerPlugin({
minimizerOptions: {
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: extendDefaultPlugins([
{
name: 'removeViewBox',
active: false,
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }],
},
},
]),
},
],
],
},
}),
]
}
优化代码性能
1. Code Split 代码分割
在进行打包时,会将所有的js文件打包到一个文件中,导致体积太大,加载速度慢。当使用了代码分割之后,生成多个js文件,渲染哪个页面就加载哪个js文件,这样就会减少资源的加载,速度就更快,从而提升性能。
使用:
一、多入口、多输出
javascript
module.exports = {
entry: { // 多个入口文件,打包时就会产生多个输出文件
main: './main.js',
app: './src/app.js'
},
optimization: {
splitChunks: { // 代码分割配置
chunks: 'all', // 对所有模块都进行分割
minSize: 20000, // 生成chunk的最小体积(以bytes为单位)
minRemainingSize: 0, // 类似minSize,最后确保提取的文件大小不能为0
minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
maxAsyncRequests: 30, // 按需加载时并行加载文件的最大数量
maxInitialRequests: 30, // 入口js文件最大并行请求数量
enforceSizeThreshold: 50000, // 超过50KB一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
cacheGroups: { // 组,指哪些模块要打包到一个组
defaultVendors: { // 组名
test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
priority: -10, // 权重,数值越大权重越高
reuseExistingChunk: true, // 如果当前chunk包含已从主bundle中拆分出的模块,则它将被重用,而不是生成新的模块
},
default: { // 默认属性,没有就会使用上面的配置,此配置会覆盖上面的配置
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
}
}
}
二、单入口,多输出
javascript
module.exports = {
entry: './main.js',
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
2. Preload / Prefetch 预加载 预获取
preload:使浏览器立即加载资源;prefetch:等待浏览器空闲时开始加载资源。它们只会加载资源,并不执行且都有缓存。Preload加载优先级要高于Prefetch。
javascript
npm install --save-dev @vue/preload-webpack-plugin
javascript
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');
module.exports = {
plugins: [
new PreloadWebpackPlugin({
rel: 'preload', // 预加载的rel
as: 'script', // 预加载的资源类型
include: 'allChunks' // 预加载的文件范围
// 或者使用 prefetch
rel: 'prefetch'
})
]
}
3. Network Cache
对输出资源的文件更好的命名,可以做好缓存,提升性能。只对修改的文件打包之后改动文件名,其它文件不因引入改动的文件而改动文件名,可以更好的做到缓存。
javascript
module.exports = {
optimization: {
runtimeChunk: {
name: entrypoint => `runtime~${entrypoint.name}.js`
}
}
}
4. Core-js 解决兼容性
例如一些ES6的新语法,babel无法处理,例如async函数、promise对象、数组的一些方法等等。所以可以使用core-js专门处理ES6及以上的语法。更好的适配老款浏览器的兼容性。
javascript
npm i core-js
安装好之后在主入口引入即可
javascript
import 'core-js'
5. PWA 渐进式网络应用程序(progressive web application - PWA)
当网络断开时,就无法访问Web应用了。为了提供离线访问效果,我们可以引入PWA,内部是通过Service Works技术实现的。可以将所有资源缓存到ServiceWork中,当离线时依旧可以访问。
javascript
npm install workbox-webpack-plugin --save-dev
javascript
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
title: 'Progressive Web Application',
}),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何"旧的" ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
最后,需要在主入口文件中注册Service Worker才能生效:
main.js
javascript
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}