分包是小程序给出的类似于web异步引入的一个方案,把一些初始进入时不需要的页面放到分包里,跳转到对应页面时再去下载分包,从而有效减少主包体积。
项目背景:
公司的小程序项目使用taro来实现一码多端,公共库和基础库放在了主包,导致主包体积体积超出了2M,无法本地预览。本次就是记录一下包体积优化的分析过程和解决思路。
小程序在app.json的subpackages字段声明分包结构:
原本的项目路径:
├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils
分包:
{
"pages":[
"pages/index",
"pages/logs"
],
"subpackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"name": "pack2",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
说明:
字段 | 类型 | 说明 |
---|---|---|
root | String | 分包根目录 |
name | String | 分包别名,分包预下载时可以使用 |
pages | StringArray | 分包页面路径,相对于分包根目录 |
independent | Boolean | 分包是否是独立分包 |
使用分包后的打包原则:
也就是说,主包用来放启动页/tabBar页面,以及公共资源和js脚本,而分包则根据开发者的配置进行划分。在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。
项目打包后,在开发者工具->详情->基本信息->本地代码->代码依赖分析,点击后即可看到主包和各个分包的体积大小。
可以看到主包的体积已经超过2M了,这就必须要对包体积进行优化,否则无法本地预览和发布。
但是我们光看到vendors.js文件体积大是不管用的,我们得知道到底是vendors.js下面的哪些文件占用的体积多,从而才能更好的优化。这就需要借助其他一些工具,如webpack-bundle-analyzer这样的一个webpack插件去做辅助分析,它可以直观的分析出打包的文件包含哪些,大小占比,模块包含关系,依赖项,文件是否重复,压缩后大小如何等等情况。
(1)介绍
本项目的taro版本为3.1.4,taro使用webpack作为内部的打包系统。有时候我们在业务代码中使用了require语法或者import default语法,webpack并不能给我们提供tree-shaking的效果。这时我们需要webpack-bundle-analyzer插件,该插件会在浏览器打开一个可视化的图表页面告诉我们引用各个包的体积。
(2)配置
// 引入依赖
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const config = {
...
mini: {
webpackChain (chain, webpack) {
chain.plugin('analyzer').use(BundleAnalyzerPlugin)
}
}
}
编译后我们就可以看到具体的包图了:
开始对上图进行分析:
关于哪些文件会打包到vendors.js中以及taro项目为什么会出现重复打包的问题
在开发小程序时,taro编译器依赖SplitChunksPlugin插件抽取公共组件,默认主包、分包依赖的公共库都会打包到根目录vendors.js文件中(有一个例外,当只有分包里面有且只有一个页面依赖公共库时,会打包到分包中依赖页面源码中),直接影响到小程序主包大小,很容易超出2M的限制大小。
只要被两个chunk引用的文件,就被打包到主包的common,而分包的每一个页面打包完后都是一个独立的chunk,那就是只要分包里有两个页面引用了同一个文件,这个文件就会被打包到common.js。
方法:配置路径别名
module.exports = {
alias: {
'bn.js': path.resolve(process.cwd(), 'node_modules', 'bn.js')
},
}
重新打包,发现bn.js只打包了一次:
项目中只有少数几个文件用到了taro-ui的组件,在具体引用时是这样做的:
//引用单个ui组件
import { AtButton } from 'taro-ui'
//全局引入样式(css中)
@import "~taro-ui/dist/style/index.scss";
但是在排查样式时发现全局引用了两次样式:
删除其中一个再次打包:
对比第一次的app.wxss减少了300多k。
而且在打包的时候,taro-ui已经被全部打包进去了,webpack并没有tree-shaking掉未引用的组件,也就是说,官方的按需引入实际上并无法实现。
看了相关的资料以及github上关于这个问题的解决方案:
taro-ui打包问题优化
github关于taro-ui按需引入的解决方案
先采用了最简单的方案,在alias添加:
alias: {
'taro-ui$': 'taro-ui/lib/index',
},
这样就可以直接加载taro-ui/lib/index中相关的组件,未加载也会被优化掉。
这么做之后直接把taro-ui/dist给干掉了。但是实际上只对js进行按需引入的话是不够的,还想对样式进行按需引入,因为项目的app.wxss和common.wxss体积还是很大,因此考虑第二个方案。
第二个方案,也就是链接一中的方案。
cnpm i babel-plugin-import --save-dev
在babel.config.js中进行如下配置:
const { includes } = require("lodash");
module.exports = {
plugins: [
'@babel/plugin-proposal-optional-chaining',
["import", {
libraryName: "taro-ui",
customName: (name, file) => {
const nameSection = name.split('-')
if (nameSection.length === 4) {
// 子组件的路径跟主组件一样
nameSection.pop()
}
// 指定组件做路径映射
const pathMap = {
'tabs/pane': 'tabs-pane',
'modal-action': 'modal/action',
'modal-content': 'modal/content',
'modal-header': 'modal/header'
}
const path = nameSection.slice(1).join('-')
return `taro-ui/lib/components/${pathMap[path] || path}`
},
style: (name) => {
if (includes(name, '/modal')) {
return 'taro-ui/dist/style/components/modal.scss'
}
const wholePath = name.split('/')
const compName = wholePath[wholePath.length - 1]
const fix = {
'tabs-pane': 'tabs',
// 2、或者在这里写映射,这里正好跟上面的映射相反
// 'modal/action': 'modal',
// 'modal/header': 'modal',
// 'modal/content': 'modal',
}[compName]
return `taro-ui/dist/style/components/${fix || compName}.scss`
}
}]
],
presets: [
['taro', {
framework: 'react',
ts: true,
hot: false // 处理h5 babel运行报错 https://github.com/NervJS/taro/releases?after=v3.1.1
}]
]
}
删除全局引用的css样式:
// @import "~taro-ui/dist/style/index.scss";
重新打包:
可以看到app.wxss和common.wxss体积都很小了。
注意:
由于taro-ui的路径很不结构化,组件中的子组件可能又要额外的去引用,这样的话代码维护会比较麻烦,因此如果以后要在项目中新增之前没有的taro组件,一定要进行额外的配置。而且如果taro-ui版本升级,那就意味着我们可能需要根据官方的版本不断去优化配置,工作量极大且意义不大。
(1)使用optimizeMainPackage,terser-webpack-plugin和miniSplitChunksPlugin插件
这部分的优化方案在这里查看:https://taro-docs.jd.com/taro/docs/mini-split-chunks-plugin
具体的配置为:
const TerserPlugin = require("terser-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniSplitChunksPlugin = require('mini-split-chunks-plugin')
const config = {
mini: {
optimizeMainPackage: {
enable: true
},
webpackChain (chain, webpack) {
process.env.TARO_ENV === 'weapp' && chain.plugin('optimizeMainPackage').use(MiniSplitChunksPlugin).before('miniPlugin')
chain.plugin('analyzer').use(BundleAnalyzerPlugin);
chain.merge({
plugin: {
terse: {
plugin: TerserPlugin,
args: [
{
minify: TerserPlugin.swcMinify,
terserOptions: {
compress: true,
},
}
]
}
},
})
}
},
}
}
(2)替换一些体积较大的组件
如将moment.js换成day.js
以上就是小程序包体积优化的整个过程,本次已经将主包的2.54M减少到了1.76M。