打包优化
打包优化
webpack 为我们构建现代化的前端页面提供了十分巨大的帮助。然而随着页面功能越来越复杂,项目体积越来越庞大时,webpack 的打包时间也会变得越来越长,打包体积也会越来越大,长时间的构建和大体积的文件会造成一些效率与性能上的问题。所以我们有必要了解一些 webpack 打包优化的技巧。
优化技巧
可以结合 webpack 配置文件的打包配置及项目的实际情况来做针对性的优化,下面将列举出一些常用的 webpack 优化技巧。
缩小搜寻范围
- 优化 loader 配置:在 loader 配置中使用
include
或者exclude
属性来包括/排除需要解析的文件。module.exports = { module: { rules: [ { // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,缩小匹配范围 test: /\.js$/, use: ['babel-loader'], // __dirname 表示当前工作目录,也就是项目根目录 // 只对项目根目录下的 src 目录中的文件采用 babel-loader 解析 include: path.resolve(__dirname, 'src'), // 也可以使用 exclude 属性来排除不需要解析的目录 }, ] }, }
- 优化 resolve.module 配置:确定模块的查找目录,避免多层查找(webpack 的模块查找机制是从
./node_modules
目录下开始查找,如果没找到再去上一层的../node_modules
查找,还是没有找到的情况下再去../../node_modules
查找)。module.exports = { resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤 modules: [path.resolve(__dirname, 'node_modules')] }, };
- 优化 resolve.mainFields 配置:确定第三方模块的入口文件字段,避免多次搜索,resolve.mainFields 的默认值与 target 的配置有关:
- 当 target 为 web 或者 webworker 时,值是
["browser", "module", "main"]
- 当 target 为其它情况时,值是
["module", "main"]
module.exports = { resolve: { // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤 mainFields: ['main'], }, };
- 当 target 为 web 或者 webworker 时,值是
- 优化 resolve.alias 配置:设置路径别名,减少解析时间。
module.exports = { resolve: { // 使用 alias 把设置路径别名,减少耗时的递归解析操作 alias: { '@':path.resolve(__dirname,'src') } }, };
- 优化 resolve.extensions 配置:减少文件类型后缀,缩小匹配范围。
module.exports = { resolve: { // 尽可能的减少后缀尝试的可能性 extensions: ['js','json'], }, };
合理利用缓存
合理的利用缓存能够大幅度的提升打包的速度。webpack 在启动打包流程时会经历一系列的解析、编译的步骤,我们可以将这些步骤中生成的一些资源缓存起来,当再次编译时,可以直接利用这些缓存资源从而提升打包编译的效率。
使用缓存的方式有以下几种:
- 使用
cache
属性设置缓存:module.exports = { cache: { // 缓存生成的 webpack 模块和 chunk,改善构建速度。 // 与 cache: true 效果相同 type: 'filesystem', }, };
- 使用 cache-loader 设置缓存:
module.exports = { module: { rules: [ { test: /\.js$/, //设置解析文件的范围 include: path.resolve("src"), use: [ { loader: "cache-loader", options:{ // 指定缓存文件存放的目录,默认目录是 'node_modules/.cache/webpack' cacheDirectory: path.resolve(".cache") } } ] } ] } }
- 使用
babel-loader
时开启缓存:module.exports = { module: { rules: [ { test: /\.js$/, //设置解析文件的范围 include: path.resolve("src"), use: [ { loader: "babel-loader", options:{ // 指定缓存文件存放的目录,默认目录是 'node_modules/.cache/babel-loader' cacheDirectory: true } } ] } ] } }
- 使用
terser-webpack-plugin
开启缓存:module.exports = { plugins: [ new TerserWebpackPlugin({ //开启压缩插件的缓存设置 cache: true, }), ], }
使用 DLLPlugin
DLL 的含义是 动态链接库,一个动态链接库中可以包含给其他模块调用的函数和数据。由此我们就可以很容易的想到将那些稳定的、不需要重复编译的第三放库设置为 DLL,这样就可以在 webpack 启动时编译一次,之后就不再需要编译,而是直接从 DLL 中获取,从而大幅提升构建速度了。
基于以上的想法,我们可以使用 DLLPlugin 和 DllReferencePlugin,它可以帮助我们完成 DLL 的设置和使用:
- 设置 DLL:设置 DLL 时会生成
manifest.json
文件,此文件描述了当前的 DLL 中包含哪些模块以及模块的路径和名称。(DLL 需要先单独打包构建然后才能给项目中的 webpack 配置使用,下面将以 vue 为例来构建 DLL)。const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin'); module.exports = { // 入口文件 entry: { // 把 vue 相关模块的放到一个单独的动态链接库 vue: ['vue','vue-router'], }, output: { // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称, // 也就是 entry 中配置的 vue filename: '[name].dll.js', // 输出的文件都放到 dist 目录下 path: path.resolve(__dirname, 'dist'), // 存放动态链接库的全局变量名称,例如对应 vue 来说就是 _dll_vue // 之所以在前面加上 _dll_ 是为了防止全局变量冲突 library: '_dll_[name]', }, plugins: [ // 使用 DllPlugin new DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中保持一致 // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值 // 例如 vue.manifest.json 中就有 "name": "_dll_vue" name: '_dll_[name]', // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, 'dist', '[name].manifest.json'), }), ], };
- 使用 DLL:将生成的
manifest.json
文件路径作为参数传递给 DllReferencePlugin。const path = require('path'); const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); module.exports = { plugins: [ // 告诉 Webpack 使用了哪些动态链接库 new DllReferencePlugin({ // 描述 vue 动态链接库的文件内容 manifest: require('./dist/vue.manifest.json'), }), ], };
使用多线程打包
webpack 在解析与编译文件时会产生大量的计算任务,由于 JS 本身是单线程执行的,这些数量庞大的计算任务就会非常耗时从而减慢打包速度,因此我们可以使用thread-loader 来开启多线程打包,将计算任务放在其他线程执行从而提升打包速度。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
{
loader: "thread-loader",
// 有同样配置的 loader 会共享一个 worker 池,thread-loader 必须放在首位
options: {
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
// 在 require('os').cpus() 是 undefined 时回退至 1
workers: 2,
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 50,
// 额外的 node.js 参数
workerNodeArgs: ['--max-old-space-size=1024'],
// 允许重新生成一个僵死的 work 池
// 这个过程会降低整体编译速度
// 并且开发环境应该设置为 false
poolRespawn: false,
// 闲置时定时删除 worker 进程
// 默认为 500(ms)
// 可以设置为无穷大,这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000,
// 池分配给 worker 的工作数量
// 默认为 200
// 降低这个数值会降低总体的效率,但是会提升工作分布更均一
poolParallelJobs: 50,
// 池的名称
// 可以修改名称来创建其余选项都一样的池
name: "my-pool"
},
},
// 一些耗时的 loader
'babel-loader',
'sass-loader',
];
},
],
},
};
压缩代码
我们在开发环境下编写的代码为了清晰与美观可能会存在大量的空格与空行,这些空白的地方在文件中也会占据一定的体积从而在打包编译时造成不必要的空间浪费。此时我们可以使用 TerserWebpackPlugin 来压缩我们的代码从而减小文件体积,提升打包速度。
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
// webpack 优化选项
optimization: {
// 开启压缩
minimize: true,
// 具体压缩方式
minimizer: [
// 使用 TerserPlugin 压缩文件
new TerserPlugin({
test: /\.js(\?.*)?$/i,
// 设置压缩文件的范围
include: path.resolve('src'),
// 开启多进程压缩
parallel: true,
}),
],
},
};
使用 CDN
在我们的项目中,一些第三方库往往是稳定的,不需要经过我们再次打包编译,而且如果你不想将这些第三方库设置为 DLL,那么也可以使用 CDN 的方式来引用这些第三放库。
CDN:内容分发网络,即在不同位置的服务器上缓存资源,将离用户最近的服务器上的资源发送给用户,从而提高响应速度,降低网络堵塞。
使用 CDN 需要配合 externals
属性来进行设置:
module.exports = {
// 使用 externals 选项来告诉 webpack 不要将模块中的这些依赖打包到最终 bundle 产物中去,
// 而是在 CDN 中引用它们
externals: {
// externals 配置对象的 key 对应模块中引入的依赖的名称
// externals 配置对象的 value 对应 CDN 提供的全局对象的名称
"vue": "Vue",
"vue-router": "VueRouter",
},
};
// 在 index.html 文件中通过 script 标签引用它们
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.29/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.5/dist/vue-router.global.min.js"></script>
拆分 CHUNKS
在 webpack 输出打包结果时,可能会将所有依赖打包为一个 chunks,这会导致 chunks 体积过大从而影响解析加载的速度,我们可以使用 webpack 的 optimization.splitChunks
属性来设置拆分 chunk,减小 chunks 的体积。
module.exports = {
// webpack 优化选项
optimization: {
// 拆分 chunks 设置
splitChunks: {
// 设置要拆分的 chunk 类型
chunks: 'async',
// 设置拆分的 chunk 的最小体积
minSize: 20000,
// 设置拆分后剩余的最小 chunk 体积限制来避免大小为零的模块
// 开发模式下默认为 0,其他情况下为 minSize 的值
minRemainingSize: 0,
// 拆分前必须共享模块的最小 chunk 数
minChunks: 1,
// 按需加载 chunk 时的最大并行请求数
maxAsyncRequests: 30,
// 起始入口点的最大并行请求数
maxInitialRequests: 30,
// 强制执行拆分的体积阈值
enforceSizeThreshold: 50000,
// 设置 chunk 拆分的缓存组
cacheGroups: {
// 缓存组中拆分的 chunk 名称
defaultVendors: {
// 拆分的 chunk 中要匹配的模块
test: /[\\/]node_modules[\\/]/,
// chunk 缓存组的优先级,优先级越高越先考虑优化
priority: -10,
// 是否复用已经拆分过的模块
reuseExistingChunk: true,
},
// 缓存组中拆分的 chunk 默认名称
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
按需加载模块
我们在浏览器中访问网页时,往往最先加载的是网站的首页,此时我们可以先加载网站首页所需要的资源,而剩余的其他页面的资源可以等到我们需要时在进行加载。 实现按需加载的关键是使用动态导入语句 import()
以及魔法注释。下面以 vue-router 为例来实现按需加载。
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from './Home.vue'
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about', //当匹配到 about 路由时在加载 about 模块
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './about.vue')
},
]