小程序性能优化从启动和运行两方面进行优化
启动性能
代码包体积
1. 合理使用分包加载(住包、独立分包、其他分包)
分包加载是小程序体积的和启动耗时优化最明显的手段。可按功能、使用频率和场景划分分包。
- 承载更多功能;提升代码包体量,承载更多功能服务
- 降低仔仔耗时:明显减少需下载的代码包大小,在不影响功能正常使用的前提,有效降低启动耗时
- 降低代码注入耗时:未开启
按需注入
,小程序编译时会将所有js文件打包成同一个文件一次性注入,并执行所有(页面和自定义组件)代码逻辑。分包可以降低注入和实际执行的打码量,从而降低耗时。 - 降低页面渲染耗时:分包可以避免不必要的组件和页面初始化
- 降低内存占用:分包能够实现页面、组件和逻辑叫粗粒度的按需加载。从而降低内存占用
- 独立分包:独立分包可以独立于住包和其他分包,从独立分包进入小程序时,可以不加载住包和其他分包。通常将功能不是很复杂且相对独立、对启动性能要求很高的页面
(如活动页、支付页、推广页···)
归于独立分包
中。
在app.json
的subpackages
字段中对应的分包配置项中定义independent
字段声明对应分包为独立分包。【moduleB】为独立分包。
限制:
- 当用户从独立分包进入小程序时,app并不一定被注册,因此getApp()不一定能获得App对象。
- 基础库2.2.4开始支持getApp({allowDefault: true}),在App未定义时返回默认实现。当主包加载,App被注册后,默认实现中定义的属性会被覆盖合并到真正的App中。
- 在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性
- 独立分包中
js
const app = getApp({allowDefault: true}) // {}
app.data = 456
app.global = {}
- 主包中
js
// app.json
{
"pages": [
"pages/index",
"pages/logs"
],
"subpackages": [
{
"root": "moduleA",
"pages": [
"pages/rabbit",
"pages/squirrel"
]
}, {
"root": "moduleB",
"pages": [
"pages/pear",
"pages/pineapple"
],
"independent": true
}
]
}
- 分包预加载:当分包页跳转到其他分包页时,需要等到其他分包下载完成后才能进入页面,造成页面切换迟缓,影响小程序体验。分包预加载解决此延迟问题。 预下载分包行为在进入某个页面时触发,通过在
app.json
增加preloadRule
配置来控制。
js
// app.json
"pages": ["pages/index"],
"subpackages": [
{
"root": "important",
"pages": ["index"],
},
{
"root": "indep",
"pages": ["index"],
"independent": true
}
],
"preloadRule": {
"pages/index": {
"network": "all",
"packages": ["important"]
},
"sub3/index": {
"packages": ["path/to"]
},
"indep/index": {
"packages": ["__APP__"]
}
}
preloadRule
中,key
是页面路径,value
是进入此页面的预下载配置,每个配置有以下几项:
字段 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
packages | StringArray | 是 | 无 | 进入页面后预下载分包的 root 或 name 。__APP__ 表示主包。 |
network | String | 否 | wifi | 在指定网络下预下载,可选值为: all : 不限网络 wifi : 仅wifi下预下载 |
- 分包异步化:
分包异步化
将小程序的分包从页面粒度细化到组件甚至文件粒度。使原本只能放在主包的部分插件、组件和代码逻辑可以剥离到分包中,并在运行时异步加载,进一步降低启动所需加载的包大小和代码量。分包异步化解决主包大小过度膨胀问题
跨分包JS代码引用:一个分包中的代码引用其它分包的代码时,为了不让下载阻塞代码运行,我们需要异步获取引用的结果。且配置占位组件 componentPlaceholder
字段。
javascript
// subPackageA/index.js
// 使用回调函数风格的调用
require('../subPackageB/utils.js', utils => {
console.log(utils.whoami) // Wechat MiniProgram
}, ({mod, errMsg}) => {
console.error(`path: ${mod}, ${errMsg}`)
})
// 或者使用 Promise 风格的调用
require.async('../commonPackage/index.js').then(pkg => {
pkg.getPackageName() // 'common'
}).catch(({mod, errMsg}) => {
console.error(`path: ${mod}, ${errMsg}`)
})
2. 避免非必要的全局自定义组件和插件
在APP.JSON中通过usingCompenents全局引入的自定义组件和plugins全局引入的插件,会在小程序启动时随主包一起下载和注入JS代码,影响启动耗时。
- 页面组件应在页面的配置json文件中配置
- 插件仅在某个分包中使用,应仅在分包中引用插件
3. 控制代码包内的资源文件
小程序代码包下载时会使用ZSTD算法进行压缩,图片、音频、视频、字体等资源文件占用较多体积。建议将代码包中此类资源尽可能部署到CDN,并使用URL引入
4. 及时清理无用代码和资源
除工具默认忽略和开发者声明忽略的文件外,小程序打包会将工程目录下所有文件都打入代码包内。意外的第三方库、废弃的代码、产品环境不需要的测试代码、未使用的组件、插件、扩展库等这些文件资源将影响代码包大小。
建议使用开发者工具提供的【代码静态依赖分析】,不定期分析包文件的构成和依赖关系,清理未使用的代码文件。也可使用工具设置的packOptions.ignore配置忽略规则
代码注入
- 按需注入:启用按需注入后,页面json和app.json中的usingConmponents配置的自定义组件,都会被视为页面依赖并进行注入和加载。需及时移除json中未使用的组件,并尽量避免全局声明属于频率低的自定义组件
js
// app.json
{
// ...
"lazyCodeLoading": "requiredComponents"
}
- 用时注入:在开启按需注入前提下,可通过【用时注入】特性,将组件在真正渲染时才进行代码注入,进一步降低小程序的启动和首屏时间。
在已经指定 lazyCodeLoading
为 requiredComponents
的情况下,为自定义组件配置 占位组件 componentPlaceholder
,组件就会自动被视为用时注入组件。
js
// app.json
{
"usingComponents": {
"comp-a": "../comp/compA",
"comp-b": "../comp/compB",
"comp-c": "../comp/compC"
},
"componentPlaceholder": {
"comp-a": "view",
"comp-b": "comp-c"
}
}
- 启动过程中尽量减少同步API的调用,避免阻塞当前JS线程(常见API: getSystemInfo/getSystemInfoSync、getStorageSync/setStorageSync)
在小程序启动流程中,代码同步执行 APP.onLaunch、App.onShow、Page.onLoad、Page.onShow
- 避免启动过程中进行复杂运算,阻塞当前JS线程,影响启动耗时
首屏渲染
- 使用【按需注入------lazyCodeLoading】和【用时注入------componentPlaceholder】结合
js
// app.json
{
"lazyCodeLoading": "requiredComponents",
"usingComponents": {
"comp-a": "../comp/compA",
"comp-b": "../comp/compB",
"comp-c": "../comp/compC"
},
"componentPlaceholder": {
"comp-a": "view",
"comp-b": "comp-c"
}
}
- 启用初始化渲染缓存------initialRenderingCache
js
// app.json
{
"initialRenderingCache": "static"
}
- 避免引用未使用的自定义组件。及时清理usingComponents对象
- 精简渲染数据
- 渐进式渲染,优先展示页面关键部分,延迟渲染非关键部分
- 与视图无关的数据尽量不放在data中
- 提前首屏数据请求
- 缓存请求数据:getStorage,setStorage
- 骨架屏
其他
小程序版本更迭,会发生如下,影响启动耗时。需合理规划版本发布
- 用户需要重新获取小程序的基础信息
- 进行小程序代码包的增量更新
- 重新生成JS代码包的Code Cache
- 重新生成初始化渲染缓存
运行时性能
1. 合理使用 setData
小程序时逻辑层和视图层双线程运行,不能直接进行数据共享,需要进行数据序列化、跨线程\进程的数据传输、数据反序列化,因此数据传输是异步、非实时的。setData流程:
- 逻辑乘虚拟DOM树的遍历和更新,触发组件生命周期和observer等
- 将data从逻辑层传输到视图层
- 视图层虚拟DOM树的更新、真实DOM元素的更新并触发页面渲染更新
- data中应只包含渲染相关的数据:setData应只对渲染数据进行更新
- 控制setData使用频率:仅在需要页面更新时调用setData,对连续的setData调用尽可能合并
(避免逻辑层JS线程持续繁忙,无法及时响应事件)
- 选择合适的setData调用范围:组件的setData调佣只会引起当前组件和子组件的更新,对频繁更新的元素封装为独立组件(如倒计时)
- setData应只传发生变化的数据:嵌套对象中数据改变,应精准到具体字段调用setData
(传输数据量会影响通讯耗时)
- 控制后台页面的setData:页面切后台后,应尽量避免setData或延迟到onShow再执行
2. 渲染性能优化
- 适当监听页面和组件得scrpll事件:非必要不使用;实现滚动相关动画时优先考虑滚动驱动动画(scroll-view)或WXS响应事件。不添加
onPageScrolll(){}
空事件 - 选择高性能恶的动画实现方式:优先CSS渐变、动画或框架提供得动画实现方式;WXS调整系节点style属性
- 使用IntersectionObserver监听元素曝光
- 控制WXML节点数量和层级数量
- 控制Page在构造时传入的自定义数据量:为了保证自定义数据在不同的页面也是不同实例,框架在页面创建时将这部分数据进行深拷贝,如果数据过多或者过于复杂,将带来很大的性能开销
3. 页面切换优化
视图层加载步骤:
- 创建Webview
- 注入视图层的小程序基础库
- 注入主包的公共代码(独立分包除外)
- 注入分包的公共代码(若页面位于分包中)
- 注入页面代码 预加载步骤仅有前三步:
- 创建Webview
- 注入视图层的小程序基础库
- 注入主包的公共代码(独立分包除外) 优化页面切换性能:
- 避免在 onHide和onUnload执行耗时操作
- 首屏渲染优化
- 提前发起数据请求(页面之间可通过EventChannel进行通信)
- 控制预加载时机:handleWebviewPreload------static、auto、manual
4. 资源加载优化
- 图片音频等静态资源压缩且放置于CDN
- 列表时使用
懒加载
优化 - 避免滥用Image组件的widthFix和heigjtFix模式,导致图片加载完成后再动态改变宽高,从而导致页面重排,发生
抖动、卡顿
现象 - 页面背景或banner,应尽量预先指定宽高,避免图片加载后在进行二次尺寸调整
5. 内存优化
- 合理使用分包加载
- 使用
按需注入
和用时注入
- 编辑器的内存分析,及时清理冗余代码
- 及时解绑页面监听事件、清理定时器等,防止内存泄漏