如今面试中问到最多的问题就是性能优化了,我们可以站在不同的角度去回答,比如减少http请求、用户体验上的优化等等很多方面。
webpack做性能优化主要从 优化构建后的结果 和 优化构建时的速度 两方面入手,本篇围绕 webpack
的分包来优化构建后的结果。
为什么说分包可以实现更好的代码组织和性能优化呢?
代码组织:分包通常基于模块化开发的原则,将应用程序拆分成独立的模块或组件。每个分包可以包含与特定功能或页面相关的代码。这种模块化组织方式使代码更易于理解、维护和扩展,因为每个模块都有明确定义的职责。
性能优化:分包后主包的体积减小,可以减小每个页面或模块的初始加载时间,因为用户只需下载当前页面或模块所需的代码,而不必加载整个应用程序的代码。这降低了首次加载的时间,提高了用户体验。
配置多入口打包
在一个大型应用中,不同页面或模块可能具有不同的代码需求。通过配置多个入口起点,您可以将每个页面或模块的代码分开打包,使代码更具可维护性和清晰度。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
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')
} ,
plugins: [
new CleanWebpackPlugin(), // 每次重新打包自动删除之前的build文件夹
new HtmlWebpackPlugin() // 对index.html进行打包处理
],
};
当进行多入口打包时,可能会遇到一个问题:如果main.js
和app.js
都依赖了相同的库,那么构建后的两个包都会包含相同的库。为了解决这个问题,可以考虑对它们共同依赖的库进行共享。
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') }
};
动态导入分包
当代码中存在不确定会被使用的模块时,最佳做法是将其分离为一个独立的 JavaScript 文件。这样可以确保在不需要该模块时,浏览器不会加载或处理该文件的 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')
})
但是我们会发现一个问题,从包名中无法区分是哪个文件构建后的包,我们可以通过webpack.config.js
中的output.chunkFilename
属性和webpack魔法注释来实现。
// main.js
homeBtn.addEventListener('click', () => {
import(/* webpackChunkName: "home" */'./views/home.js') // 让webpack读取的魔法注释,固定写法
})
aboutBtn.addEventListener('click', () => {
import(/* webpackChunkName: "about" */'./views/about.js')
})
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build'),
chunkFilename: 'chunk_[name]_[id].js',
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()],
};
另外还有两种优化动态导入分包性能的方式prefetch
和preload
,两种有如下区别
prefetch
(预获取): 被用于懒加载策略。它会在浏览器空闲时,即浏览器已经加载主要资源并且有剩余带宽时,开始加载。这意味着它不会影响初始页面加载时间,因为它是在后台加载的。通常用于加载将来可能需要的资源,例如懒加载的代码块或其他不太紧急的资源。preload
(预加载): 用于立即加载重要资源。它会在当前页面加载时立即开始加载,而不管浏览器的空闲状态如何。因此,preload
可能会影响初始页面加载性能,因为它可以竞争主要资源的带宽。通常用于加载当前页面渲染所必需的关键资源,如字体、样式表或脚本。
使用方式也是通过魔法注释
import(/* webpackPrefetch: true */ './view/home');
import(/* webpackPreload: true */ './view/about');
SplitChunks 插件分包
另外一种分包的模式是splitChunk
,通过配置SplitChunks
,你可以控制哪些模块应该被拆分,以及如何拆分它们。这有助于减小生成的JavaScript文件的大小,提高应用程序的性能,并降低加载时间。该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件,只需要提供SplitChunksPlugin
相关的配置信息即可。
splitChunks.chunks
中有三个属性,分别是
async
:只拆分异步导入的模块。initial
:只拆分同步导入的模块。all
:拆分所有模块,无论是同步还是异步导入的。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { resolve } = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'build')
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 100, // 生成 chunk 的最小体积(以 bytes 为单位)
maxSize: 10000, // 将大于maxSize的包,拆分成不小于minSize的包(以 bytes 为单位)
cacheGroups: { // 用于对拆分的包进行分组
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 通过正则匹配node_modules
name: 'vender', // 用在filename中的name占位符
filename: '[name]_[id].js',
},
},
},
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()],
};
更多的配置详见SplitChunksPlugin配置
将css提取到一个独立的css文件
我们平时在打包css文件时,是通过css-loader
和style-loader
进行如下webpack.config.js
配置就会打包到主包中
npm install css-loader -D
npm install style-loader -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
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' }
]
}
]
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()],
};
如果将css单独打包到一个css文件中有如下好处:
- 分离结构和样式:将CSS独立出来可以将网页的结构(HTML)和样式(CSS)分开,使代码更加模块化和易于维护。
- 缓存优化:独立的CSS文件可以被浏览器缓存,当用户再次访问网站时,可以减少加载时间,提高性能。
我们要想将css提取到一个独立的css文件中可以使用MiniCssExtractPlugin
插件,该插件需要在webpack4+才可以使用。
安装 mini-css-extract-plugin:
npm install mini-css-extract-plugin -D
在webpack.config.js文件中配置rules和plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
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 CleanWebpackPlugin(),
new HtmlWebpackPlugin(),
new MiniCssExtractPlugin({ // 使用MiniCssExtractPlugin插件
filename: "css/[name]_[id].css", // 打包后的css文件放到css文件夹中
chunkFilename: "css/[name]_[id].css"
}
)
],
};
总结
这些分包的方法可以根据项目的需求和情况进行选择和组合,以达到最佳的代码组织和性能优化效果。