产物拆包
一、认识
一般来说,如果不对产物进行代码分割(或者拆包),全部打包到一个 chunk
中,会产生如下的问题:
-
首屏加载的代码体积过大,即使是当前页面不需要的代码也会进行加载。
-
线上缓存复用率极低,改动一行代码即可导致整个
bundle
产物缓存失效。
Vite
分包策略如下:
-
默认分包策略:
Vite
基于Rollup
的manualChunksAPI
实现了应用拆包的策略为: 实现了自动CSS
代码分割的能力,即实现一个chunk
对应一个css
文件; 对于Async Chunk
而言 ,动态import
的代码会被拆分成单独的chunk
; 对于普通的业务代码和第三方代码, 会打包到一个Chunk
中 -
manualChunks
: 针对更细粒度的拆包,Vite
的底层打包引擎Rollup
提供了manualChunks
,让我们能自定义拆包策略 -
vite-plugin-chunk-split
:
二、默认拆包策略
Vite
基于 Rollup
的 manualChunksAPI
实现了应用拆包的策略为: 实现了自动 CSS
代码分割的能力,即实现一个 chunk
对应一个 css
文件; 对于 Async Chunk
而言 ,动态 import
的代码会被拆分成单独的 chunk
; 对于普通的业务代码和第三方代码, 会打包到一个 Chunk
中
三、分包策略
针对更细粒度的拆包,Vite
的底层打包引擎 Rollup
提供了manualChunks
,让我们能自定义拆包策略。Rollup
会对每一个模块调用 manualChunks
函数,在 manualChunks
的函数入参中你可以拿到模块 id
及模块详情信息,经过一定的处理后返回 chunk
文件的名称,这样当前 id
代表的模块便会打包到你所指定的 chunk
文件中。
3.1 对象配置
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 将 Lodash 库的代码单独打包
lodash: ['lodash-es'],
// 将 React 相关库打包成单独的 chunk 中
'react-vendor': ['react', 'react-dom'],
// 将组件库的代码打包
library: ['antd', '@arco-design/web-react']
}
}
}
}
});
在对象格式的配置中,key
代表 chunk
的名称,value
为一个字符串数组,每一项为第三方包的包名。我们可以执行 npm run build
尝试一下打包, 你可以看到原来的 vendor
大文件被拆分成了我们手动指定的几个小 chunk
,每个 chunk
大概 200 KB
左右,是一个比较理想的 chunk
体积。这样,当第三方包更新的时候,也只会更新其中一个 chunk
的 url
,而不会全量更新,从而提高了第三方包产物的缓存命中率。
3.2 函数配置
除了对象的配置方式之外,我们还可以通过函数进行更加灵活的配置,而 Vite
中的默认拆包策略也是通过函数的方式来进行配置的
function createMoveToVendorChunkFn() {
return (id) => {
if (id.includes('antd') || id.includes('@arco-design/web-react')) {
return 'library';
}
if (id.includes('lodash')) {
return 'lodash';
}
if (id.includes('react')) {
return 'react';
}
};
}
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: createMoveToVendorChunkFn()
}
}
}
});
Rollup
会对每一个模块调用 manualChunks
函数,在 manualChunks
的函数入参中你可以拿到模块 id
及模块详情信息,经过一定的处理后返回 chunk
文件的名称,这样当前 id
代表的模块便会打包到你所指定的 chunk
文件中。但是 Vite
这样的默认配置存在一定的问题, manualChunks
逻辑过于简单粗暴,仅仅通过路径 id
来决定打包到哪个 chunk
中,而漏掉了间接依赖的情况。比如说: 像 object-assign
这种 react
本身的依赖并没有打包进react-vendor
中,而是打包到另外的 chunk
当中,从而导致如下的循环依赖关系:
解决思路: 如果针对像 object-assign
这种间接依赖,我们也能识别出它属于 react
的依赖,将其自动打包到react-vendor
中,这样就可以避免循环引用的问题。解决思路如下:
-
确定
react
相关包的入口路径。 -
在
manualChunks
中拿到模块的详细信息,向上追溯它的引用者,如果命中react
的路径,则将模块放到react-vendor
中。
import { defineConfig } from 'vite';
import inspect from 'vite-plugin-inspect';
import react from '@vitejs/plugin-react-swc';
const baseMap: { [key: string]: string } = {
development: '/',
production: '/test/vite/cssEngineering/preprocessor/dist'
};
const chunkGroups = {
'react-vendor': ['react', 'react-dom']
};
function createMoveToVendorChunkFn() {
const cache = new Map();
// 递归向上查找引用者,检查是否命中 chunkGroups 声明的包
const isDepInclude = (id, depPaths, importChain, getModuleInfo) => {
const key = `${id}-${depPaths.join('|')}`;
// 出现循环依赖,不考虑
if (importChain.includes(id)) {
cache.set(key, false);
return false;
}
// 验证缓存
if (cache.has(key)) {
return cache.get(key);
}
// 命中依赖列表
if (depPaths.includes(id)) {
// 引用链中的文件都记录到缓存中
importChain.forEach(item =>
cache.set(`${item}-${depPaths.join('|')}`, true)
);
return true;
}
const moduleInfo = getModuleInfo(id);
if (!moduleInfo || !moduleInfo.importers) {
cache.set(key, false);
return false;
}
// 核心逻辑,递归查找上层引用者
const isInclude = moduleInfo.importers.some(importer =>
isDepInclude(importer, depPaths, importChain.concat(id), getModuleInfo)
);
// 设置缓存
cache.set(key, isInclude);
return isInclude;
};
return (id, getModuleInfo) => {
for (const group of Object.keys(chunkGroups)) {
const deps = chunkGroups[group];
if (
id.includes('node_modules') &&
isDepInclude(id, deps, [], getModuleInfo)
) {
return group;
}
}
};
}
export default defineConfig({
base: baseMap[process.env.NODE_ENV],
plugins: [react(), inspect()],
build: {
rollupOptions: {
output: {
manualChunks: createMoveToVendorChunkFn()
}
}
}
});
四、vite-plugin-chunk-split
vite-plugin-chunk-split
是 Vite
终极分包策略
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { chunkSplitPlugin } from 'vite-plugin-chunk-split';
const baseMap: { [key: string]: string } = {
development: '/',
production: '/test/vite/cssEngineering/preprocessor/dist'
};
export default defineConfig({
base: baseMap[process.env.NODE_ENV],
plugins: [react(), chunkSplitPlugin({
customSplitting: {
lodash: ['lodash'],
'react-vendor': ['react'],
}
})],
});