默认情况下,Webpack 会将所有代码构建成一个单独的包,这在小型项目通常不会有明显的性能问题,但伴随着项目的推进,包体积逐步增长可能会导致应用的响应耗时越来越长。归根结底这种将所有资源打包成一个文件的方式存在两个弊端:
这些问题都可以通过代码分离解决,例如 node_modules
中的资源通常变动较少,可以抽成一个独立的包,那么业务代码的频繁变动不会导致这部分第三方库资源被无意义地重复加载。
const path = require('path');
module.exports = {
entry: {
main: './src/main.js', // 第一个入口起点
app: './src/app.js' // 第二个入口起点
},
output: {
filename: '[name].bundle.js', // 使用[name]占位符将生成的文件名与入口起点名称对应
path: path.resolve(__dirname, 'build')
} ,
};
const path = require('path');
module.exports = {
entry: {
main: { import: './src/main.js', dependOn: 'shared' }, // 第一个入口起点
app: { import: './src/app.js', dependOn: 'shared' }, // 第二个入口起点
shared: ['dayjs', 'lodash'] // 共享的库
},
output: {
filename: '[name].bundle.js', // 使用[name]占位符将生成的文件名与入口起点名称对应
path: path.resolve(__dirname, 'dist') }
};
Webpack 提供了 SplitChunkPlugin 进行分包优化。SplitChunksPlugin 插件可以将应用程序中共享的代码拆分成单独的块,以便将其从应用程序代码中分离出来,从而提高性能和加载速度。 该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件,只需要提供SplitChunksPlugin
相关的配置信息即可。
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build')
},
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
namw:'', // 用在filename中的name占位符
fileName: 'vender_[id]_[name].js', //打包之后的文件名
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
属性名 | 值 |
splitChunks.chunks |
|
splitChunks.minSize |
拆分出来的这个包的最小尺寸为minSize(以 bytes 为单位) 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
|
splitChunks.maxSize |
将大于maxSize的包, 拆分成不小于minSize的包 |
splitChunks.minChunks |
至少被引入的次数,默认是1;
如果我们写一个2,但是引入了一次,那么不会被单独拆分;
|
splitChunks.maxAsyncRequests |
最大的初始化请求数量 |
splitChunks.cacheGroups |
用于对拆分的包进行分组 |
priority
(优先级)的cacheGroups。默认组的优先级为负,自定义组的默认值为 0;
reuseExistingChunk:
如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。这可能会影响 chunk 的结果文件名;更多的配置可以参考:
SplitChunksPlugin | webpack 中文文档 (docschina.org)
当代码中存在不确定会被使用的模块时,最佳做法是将其分离为一个独立的 JavaScript 文件。
实现动态导入的方式是使用ES6的import()
语法来完成。
动态导入最常见的使用场景就是路由懒加载
// main.js文件中
const homeBtn = document.createElement('button')
const aboutBtn = document.createElement('button')
homeBtn.textContent = '加载home文件'
aboutBtn.textContent = '加载about文件'
document.body.appendChild(homeBtn)
document.body.appendChild(aboutBtn)
homeBtn.addEventListener('click', () => {
import('./views/home.js')
})
aboutBtn.addEventListener('click', () => {
import('./views/about.js')
})
打包之后的资源:
但是我们会发现一个问题,从包名中无法区分是哪个文件构建后的包,我们可以通过以下方式修改打包之后的文件名:
output.chunkFilename
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build'),
chunkFilename: 'chunk_[name]_[id].js',
},
};
默认情况下我们获取到的 [name] 和 [id] 的名称保持一致的,如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式;
// main.js
homeBtn.addEventListener('click', () => {
import(/* webpackChunkName: "home" */'./views/home.js') // 让webpack读取的魔法注释,固定写法
})
aboutBtn.addEventListener('click', () => {
import(/* webpackChunkName: "about" */'./views/about.js')
})
打包之后的资源,可以从名称看出原始文件是哪一个
prefetch被用于懒加载策略。它会在浏览器空闲时,即浏览器已经加载主要资源并且有剩余带宽时,开始加载。这意味着它不会影响初始页面加载时间,因为它是在后台加载的。通常用于加载将来可能需要的资源,例如懒加载的代码块或其他不太紧急的资源。
preload用于立即加载重要资源。它会在当前页面加载时立即开始加载,而不管浏览器的空闲状态如何。因此,preload
可能会影响初始页面加载性能,因为它可以竞争主要资源的带宽。通常用于加载当前页面渲染所必需的关键资源,如字体、样式表或脚本。
import(/* webpackPrefetch: true */ './view/home');
import(/* webpackPreload: true */ './view/about');
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build')
},
optimization: {
runtimeChunk: 'true/multiple' //针对每个入口打包一个runtime文件
runtimeChunk: 'single' //打包一个runtime文件
runtimeChunk: {
name: function(entrypoint) {
return `my-${entrypoint.name}` // 决定runtimeChunk的名称
}
}
},
};
我们平时在打包css文件时,是通过css-loader
和style-loader
进行如下配置,最终会把css注入到页面中,
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build'),
},
module: {
rules: [
{ //通过正则告诉webpack匹配是什么文件
test: /\.css$/,
use: [
// 因为loader的执行顺序是从右向左(或者说从下到上,或者说从后到前 的),所以我们需要将style-loader写到css-loader的前面;
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
},
};
如果将css单独打包到一个css文件中有如下好处:
npm install mini-css-extract-plugin -D
const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build'),
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 将CSS样式提取为单独的CSS文件,通过链接方式(link)引入到HTML中
{ loader: MiniCssExtractPlugin.loader },
{ loader: 'css-loader' }
]
}
]
},
plugins: [
new MiniCssExtractPlugin({ // 使用MiniCssExtractPlugin插件
filename: "css/[name]_[id].css", // 打包后的css文件放到css文件夹中
chunkFilename: "css/[name]_[id].css"
}
)
],
};
打包之后,css被单独打包到build/css/main_179.css
参考:
Webpack 优化实操(十一):页面分包优化 - 知乎 (zhihu.com)
webpack性能优化(一):分包 - 掘金 (juejin.cn)