Webpack 分包实践

什么是分包

分包（Code Splitting）是一种优化技术，用于将应用程序的代码分割成多个较小的包，而不是将所有代码打包成一个单一的文件。这种技术可以显著提高应用程序的性能和加载速度，特别是在大型应用中。

Webpack 中的分包

Webpack 经过初始化、编译、优化、输出和插件执行等多个阶段，把源码输出为一个个包（Chunk）。Webpack 中的分包行为自 Webpack4 开始是一个默认行为，并且在配置文件中可以通过optimization.splitchunks这个对象字面量来配置相关行为 (https://webpack.js.org/plugins/split-chunks-plugin/)。

Copy
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 某种意义上来讲也成了一种负优化。

Copy
1. cdn导入
//webpack.config.js

modules.exports={
externals: {
lodash: '_'
}
}

//index.html
//使用externals将lodash排除，再在模版文件中通过script加载，实际上是直接拉取了整个lodash库


2. 配置缓存组
modules.exports={
optimization.splitchunks: {
chunks: "all",
cacheGroups: {
// 最后的优化阶段，会把并未使用的lodash方法给移除掉，相较于cdn导入，需要加载的代码量无疑是更少的
lodash: {
test: [\\/]node_modules[\\/]lodash,
}
}
}
}

但是在某些场景下 cdn 仍有用武之地，比如服务器使用 http1.1 进行数据传输同时依赖较小或者全量使用依赖。

合理拆分不易变动的模块
Webpack4 的默认拆包行为会把从 node_modules 中导入的依赖全部打包到一个 Chunk 中。其中一些经常变动的依赖，比如公司内部的 sdk 或组件库可以采用默认的分包策略，但一些长期无变动的依赖，就可以使用缓存组拆分，并长期缓存在浏览器

Copy
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 () 方法来动态地导入组件

Copy
export default {
component: () => import('./A.vue'),
}

同理，动态地导入路由组件

Copy
const router = createRouter({
// ...
routes: [
{ path: '/a', component: () => import('./views/A.vue') },
]
})

一些框架内置的懒加载方法，比如 react 中的 lazy

Copy
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 (
<>
setMarkdown(e.target.value)} />

setShowPreview(e.target.checked)} />
Show preview


{showPreview && (
<Suspense fallback={<Loading />}>
Preview
<MarkdownPreview markdown={markdown} />
</Suspense>
)}
</>
);
}

function delayForDemo(promise) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
}).then(() => promise);
}