概述
首先介绍一下我们公司的前端跨端框架技术栈:
- React 18.x
- Taro 3.x
- TypeScript 5.x
- pnpm 8.x
某个大型项目开发需求,项目目录结构类似这样:
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── .prettierrc
├── README.md
├── components/
│ ├── backend-api/
│ ├── backend-pro/
│ ├── data/
│ ├── form-rules/
│ ├── react-hooks/
│ ├── request/
│ ├── taro-design/
│ ├── taro-extend-types/
│ ├── taro-utils/
│ └── utils/
├── package.json
├── packages/
│ ├── backend/
│ ├── pos/
│ ├── staff/
│ └── user/
├── patches/
│ └── jotai@2.6.0.patch
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.backend.json
├── tsconfig.json
└── tsconfig.taro.json
开发阶段都非常顺利。但是进入提测环境后,某天客户技术跟我说某个H5打包体积异常,并且给我发了这么一个截图:
我一看,好家伙!这个项目确实页面不少,但也不至于如此夸张。我只好回复一个「收到」并开始进行排查 🕵️♂️
排查过程
复现流程
首先拉取相关分支代码并进行本地打包,查看构建输出产物体积。输出结果发现 dist 确实有八十几兆。😱
那么开始排查打包 bundle,首先祭出 webpack-bundle-analyzer
分析神器。大概结果如下:
通过上面的图我们实际上已经可以发现问题了:
- 每个 chunk 体积都有一点几兆
- 查看前三个 chunk 发现每个 chunk 都引入了相同的几个包:
taro-components
、taro-design
等
基于此,我们继续查看一下 Taro 关于 splitChunk 的配置规则
查看配置
在 config/index.js
中添加如下修改:
const config = {
// 省略其他配置
h5: {
webpackChain(config, webpack) {
fs.writeFileSync(path.resolve(__dirname, 'output.js'), config.toString())
}
}
}
再次运行后查看 output.js
,可以发现相关配置如下:
{
entry: {
app: [
'D:\\projects\\xx-taro\\packages\\staff\\src\\app.config'
]
}
optimization: {
minimize: true, // 启用代码压缩
nodeEnv: 'production', // 强制设置为生产环境(会启用生产优化)
chunkIds: 'deterministic',// 生成确定性 Chunk ID(利于长效缓存)
removeEmptyChunks: true // 移除空 Chunk
splitChunks: {
chunks: 'initial', // 仅拆分同步加载的代码(异步加载的代码不处理)
hidePathInfo: true, // 隐藏路径信息(避免泄露文件结构)
minSize: 0, // 允许拆分包的最小大小为 0(强制拆分小模块)
cacheGroups: { // 自定义拆包规则
'default': false, // 禁用默认拆包规则
defaultVendors: false,// 禁用默认的 `node_modules` 拆包规则
// 自定义公共模块组
common: {
name: false, // 自动生成名称
minChunks: 2, // 被 2 个及以上 Chunk 引用的模块
priority: 1 // 优先级较低
},
// 自定义第三方库组
vendors: {
name: false,
minChunks: 2, // 被 2 个及以上 Chunk 引用的第三方库
test: (module) => /[\\/]node_modules[\\/]/.test(module.resource),
priority: 10 // 优先级高于 common
},
// Taro 框架专用组
taro: {
name: false,
test: (module) => /@tarojs[\\/][a-z]+/.test(module.context),
priority: 100 // 最高优先级(优先单独打包)
}
}
}
}
}
仔细查看 Taro 的 H5 构建配置信息,可以发现几个明显的问题:
- 拆包策略问题:
chunks: 'initial'
配合minChunks: 2
策略,但是由于 entry 只有单入口,导致 common/vendors 拆包失效 - 动态加载不匹配:H5 项目 subpackages 会处理为动态加载,不适配
initial
的 chunk 机制,导致 taro 组 chunk 也失效
// app.config.ts
// 动态加载的子包会生成独立 Chunk
export default {
pages: [
'pages/index/index',
'pages/detail/detail'
],
// 动态加载子包
subpackages: [
{
root: 'subpackage',
pages: ['pageA', 'pageB']
}
]
}
修改打包配置
综合分析:Taro H5 的构建配置是针对多入口的 chunk 规则,并不适合当前项目的构建规则。所以基于项目实际情况,我们将 chunk 规则修改如下:
{
optimization: {
splitChunks: {
// 修改为 'all' 以包含同步和异步模块
chunks: 'all',
// 设置合理的最小分割大小,避免生成过多小文件 (单位:字节)
minSize: 20000, // 示例值,可根据实际情况调整
// maxInitialRequests 和 maxAsyncRequests 也可以考虑设置,避免单个入口请求太多 chunk
// maxInitialRequests: 10, // 示例值
// maxAsyncRequests: 10, // 示例值
cacheGroups: {
default: false,
defaultVendors: false,
common: {
name: 'chunk-common',
minChunks: 2,
priority: 1,
reuseExistingChunk: true // 保持良好实践
},
vendors: {
name: 'chunk-vendors',
// minChunks: 2 在 chunks: 'all' + minSize 下也可以,取决于你期望的行为
// 1 也可以考虑,只要 minSize 够大,就不会分割很小的单用库
minChunks: 2,
test: (module) =>
/[\\/]node_modules[\\/]/.test(module.resource) &&
!/[\\/]taro-design[\\/]/.test(module.resource) &&
!/[\\/](react|react-dom|dayjs)[\\/]/.test(module.resource), // 排除已被更高优先级组捕获的模块
priority: 10,
reuseExistingChunk: true
},
react: {
name: 'chunk-react',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
priority: 102,
reuseExistingChunk: true
},
dayjs: {
name: 'chunk-dayjs',
test: /[\\/]node_modules[\\/](dayjs)[\\/]/,
priority: 102,
reuseExistingChunk: true
},
taro: {
name: 'chunk-taro',
reuseExistingChunk: true,
test: (module) => /@tarojs[\\/]/.test(module.context),
priority: 100
},
taroDesign: {
name: 'chunk-taro-design',
test: (module) => /[\\/]taro-design[\\/]/.test(module.resource),
priority: 101,
reuseExistingChunk: true
}
}
}
}
让我们重新运行一下构建命令看看效果:
文件只剩六点几兆了!优化率高达 92%!!!效果还是非常好的,可以美滋滋地交差啦 🎉
继续优化
经过上一次优化,我们的优化率已经很高了,但仍然还有优化空间。作为一个有追求的前端工程师,我们进一步排查代码,针对项目做了更加极致的优化。包括:
1. 异步加载城市数据
const [remoteData, setRemoteData] = useState<ICityData[]>([])
useEffect(() => {
const fetchData = async () => {
try {
const response = await Taro.request({
timeout: 5000, // Set a timeout for the request
url: 'https://aliyuncs.com/district.json'
})
setRemoteData(response.data)
} catch (error) {
console.error('Failed to fetch district data:', error)
}
}
if (!propData) {
fetchData()
}
}, [propData])
2. 生产环境移除调试工具
{
copy: {
patterns: [
{ from: 'src/static/', to: outputRoot + '/static/', ignore: REACT_APP_ENV === 'prod' ? ['**/vconsole.min.js'] : [] },
]
}
}
3. 防止大图片被转换为 base64
{
h5: {
imageUrlLoaderOption: {
limit: false // 禁用图片转换为base64编码
},
}
}
4. 打包时不生成 LICENSE.txt 文件
{
webpackChain(config, webpack) {
// 不生成LICENSE.txt文件
config.optimization.minimizer('terserPlugin').tap((args) => {
args[0].extractComments = false
return args
})
}
}
再次运行查看dist包体积
上图是时隔半年后迭代多轮需求后打的包
经过这一系列优化后,最终打包输出体积从原来的 80+ MB 降低到了 4-5 MB,整体优化率达到了惊人的 94%!🚀
总结
这次 Taro H5 构建包体积优化之旅,让我们深刻体会到了性能优化的重要性和技巧性。通过这个案例,我们学到了:
核心收获
-
深入理解 webpack splitChunks 机制
掌握了chunks: 'initial'
vschunks: 'all'
的区别,以及如何根据项目特点选择合适的拆包策略 -
Taro 构建配置的定制化
学会了如何查看和修改 Taro 的构建配置,针对具体项目需求进行个性化优化 -
多维度优化思路
从代码分割、资源加载、环境配置等多个角度进行优化,形成了完整的优化方案
优化策略总结
- 主要优化:修改 splitChunks 配置(优化率 92%)
- 细节优化:异步加载、移除调试工具、图片处理等(额外优化 2%)
- 最终效果:总体积从 80+ MB 降至 4-5 MB,优化率 94%
经验启示
这个案例告诉我们,工具配置的默认值并不总是最优解。当项目规模和复杂度达到一定程度时,我们需要:
- 🔍 主动分析:使用
webpack-bundle-analyzer
等工具深入分析 - 🎯 精准定位:找到问题的根本原因,而不是表面现象
- ⚡ 系统优化:从配置、代码、资源等多个维度进行优化
- 📊 量化效果:用数据说话,验证优化效果
记住:性能优化是一门艺术,需要理论与实践相结合,工具与经验并重! 💪
希望这篇文章能帮助到遇到类似问题的同学们。如果你也有类似的优化经验,欢迎分享交流!