什么是分包#
分包(Code Splitting)是一种优化技术,用于将应用程序的代码分割成多个较小的包,而不是将所有代码打包成一个单一的文件。这种技术可以显著提高应用程序的性能和加载速度,特别是在大型应用中。
Webpack 中的分包#
Webpack 经过初始化、编译、优化、输出和插件执行等多个阶段,把源码输出为一个个包(Chunk)。Webpack 中的分包行为自 Webpack4 开始是一个默认行为,并且在配置文件中可以通过optimization.splitchunks
这个对象字面量来配置相关行为 (https://webpack.js.org/plugins/split-chunks-plugin/)。
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
// 拆分从node_modules中导入的依赖
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
// 拆分公共模块
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
分包策略#
分包的大致思路有以下两种
- 动态导入(dynamic import)
- 拆分大文件,合并小文件和公共模块
动态导入#
动态导入也可以说是懒加载,在非必要的时候不拉取模块文件,只在使用时才从服务端获取相关文件。Js 内置的import()
语句、React 的lazy
方法和 vue-router 开箱即用的路由懒加载都是动态加载的实现。
包拆分与合并#
- Webpack 在输出时会根据配置的 filename 给每个 Chunk 加上 hash,合理地拆分那些不会变动的模块,文件能更长久地缓存在客户端,并且在多次打包部署中,缓存仍会有效,
- 根据网络资源,把大 Chunk 分为多个小 Chunk,能更高效地加载应用。比如 http2 下,这种操作会进一步提高网络资源的利用率
- 抽离出几个 Chunk 的公共部分,避免模块的重复注入
实践#
- cdn 与缓存组比对
cdn 导入包内容实际上也是实现长效缓存的一种方式,但随着 tree-shaking 的广泛应用,cdn 某种意义上来讲也成了一种负优化。
1. cdn导入
//webpack.config.js
modules.exports={
externals: {
lodash: '_'
}
}
//index.html
//使用externals将lodash排除,再在模版文件中通过script加载,实际上是直接拉取了整个lodash库
<script src="bootcdn/ajax/[email protected]/lodash.min.js">
2. 配置缓存组
modules.exports={
optimization.splitchunks: {
chunks: "all",
cacheGroups: {
// 最后的优化阶段,会把并未使用的lodash方法给移除掉,相较于cdn导入,需要加载的代码量无疑是更少的
lodash: {
test: [\\/]node_modules[\\/]lodash,
}
}
}
}
但是在某些场景下 cdn 仍有用武之地,比如服务器使用 http1.1 进行数据传输同时依赖较小或者全量使用依赖。
- 合理拆分不易变动的模块
Webpack4 的默认拆包行为会把从 node_modules 中导入的依赖全部打包到一个 Chunk 中。其中一些经常变动的依赖,比如公司内部的 sdk 或组件库可以采用默认的分包策略,但一些长期无变动的依赖,就可以使用缓存组拆分,并长期缓存在浏览器
modules.exports={
optimization.splitchunks: {
chunks: "all",
cacheGroups: {
vue: {
test: [\\/]node_modules[\\/](vue|vue-router|vuex),
},
lodash: {
test: [\\/]node_modules[\\/]lodash
},
element: {
test: [\\/]node_modules[\\/]element-ui
}
}
}
}
- 懒加载
通过 Js 内置的 import () 方法来动态地导入组件
export default {
component: () => import('./A.vue'),
}
同理,动态地导入路由组件
const router = createRouter({
// ...
routes: [
{ path: '/a', component: () => import('./views/A.vue') },
]
})
一些框架内置的懒加载方法,比如 react 中的 lazy
import { useState, Suspense, lazy } from 'react';
import Loading from './Loading.js';
const MarkdownPreview = lazy(() => delayForDemo(import('./MarkdownPreview.js')));
export default function MarkdownEditor() {
const [showPreview, setShowPreview] = useState(false);
const [markdown, setMarkdown] = useState('Hello, **world**!');
return (
<>
<textarea value={markdown} onChange={e => setMarkdown(e.target.value)} />
<label>
<input type="checkbox" checked={showPreview} onChange={e => setShowPreview(e.target.checked)} />
Show preview
</label>
<hr />
{showPreview && (
<Suspense fallback={<Loading />}>
<h2>Preview</h2>
<MarkdownPreview markdown={markdown} />
</Suspense>
)}
</>
);
}
function delayForDemo(promise) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
}).then(() => promise);
}