基于 Taro 的微信小程序优化指南
# 基于 Taro 的微信小程序优化指南
# 一、框架
逻辑层(App Service) + 视图层(View)
# 1、逻辑层
基于 JavaScript 引擎的基础,并额外提供了一部分小程序的功能 API,构成了小程序的逻辑层。
- 在Android上,使用 V8 引擎解析和渲染 JavaScript;在iOS上,使用 JavaScriptCore。相对来说 V8 的性能比 JSC 要好很多,同时这也是代码会有兼容问题的根本原因
- 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。
- 开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在小程序启动的时候运行,直到小程序销毁。这一行为类似 ServiceWorker,所以逻辑层也称之为 App Service。
- 小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 window,document 等
微信在 JavaScript 的基础上,增加的功能:
- 增加 App 和 Page 方法,进行程序注册和页面注册。
- 增加 getApp 和 getCurrentPages 方法,分别用来获取 App 实例和当前页面栈。
- 提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
- 提供模块化能力,每个页面有独立的作用域。
为什么开发版和体验版小程序启动比正式版性能要差?
- 开发版和体验版的启动流程和代码包下载链路会和正式版有所差别
- 开发版和体验版权限控制更严格
- 开发版为了方便开发调试,基础库会启用很多调试相关的能力,例如 vConsole、sourceMap 等,日志输出的等级也会更低
为什么安卓和 iOS 的小程序性能差异大?
- iOS 小程序和微信共用进程,而 Android 上小程序运行在独立进程,需要额外的内存创建进程
- iOS 上需要使用系统提供的 WebView 和 JavaScript Core,不需要额外产生初始化的开销
- 安卓 UI 和系统组件的创建的开销远高于 iOS
# 2、视图层
视图层的本质是一个WebView,框架的视图层由 WXML、WXS、WXSS 编写,由组件来进行展示。
# WXS 与 JavaScript 的区别
- wxs 在视图层,能减少跨线程通信的开销,而 js 在逻辑层
- 使用时类似 script 标签,但是使用时必须添加 module 属性
- wxs 有自己的数据类型与类库,比如:通过 typeof 拿到的原型是 object,只能通过 constructor 才能拿到 Array
- wxs 不支持 ES6
- wxs 遵循 commonjs 规范
- wxs 在 ios 上不会使用 JavaScriptCore,所以在 ios 上使用 wxs 会比 js 快 2 到 20 倍
# 3、架构优化
Skyline 渲染引擎(Beta) (opens new window)
// page.json
// skyline 渲染
{
"renderer": "skyline"
}
// webview 渲染
{
"renderer": "webview"
}
2
3
4
5
6
7
8
9
10
目前,Skyline 还处于 beta 测试中,还不能正式应用到线上
# 二、数据交互
小程序的框架核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。
在逻辑层通过 this.setData 修改数据,数据会传输到视图层,再做相应的更新。
# 1、Taro 转换为小程序代码的过程
- 通过 Babel 编译(Parse )代码转换为 抽象语法书(AST)
- 对 AST 进行遍历(traverse )和替换(replace)
- 根据新的 AST 生成( generate )小程序代码
# 2、事件优化
视图层将事件反馈给逻辑层时,需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,可以去掉不必要的事件绑定,减少事件触发的频率来提高性能。如:
- 只在必要时监听 onPageScroll 事件
- 不在 onPageScroll 中做复杂、耗时的逻辑处理
- 避免频繁 selectQuery 查询节点信息
- 合理的利用防抖和节流
逻辑层传输数据到视图层也或产生一定的延迟,延迟与传输数据量正相关,可以控制传输渲染需要的关键数据减少传输体积来提高性能。
# 3、setData优化
setData 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。由于小程序运行逻辑线程与渲染线程之上,setData 的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间。
优化方案:
- 每秒调用 setData 的次数不超过 20 次
- 保证 setData 的数据在 JSON.stringify 后不超过 256KB
- 只更新变化的数据
- 只设置需要渲染的数据
在 Taro 中,我们无法直接对 setData 进行操作,一切都由所使用的框架进行接管,按照框架的更新去优化即可。
如:
- Taro + React 中,只需要知道 setState 类似setData,控制 setState 的调用次数与数据大小就能控制 setData 的调用次数与数据大小
- Taro + React 中,使用 React Hooks 进行更新,只需要知道 Hooks 本质是在一个链表中去进行 setData,而需要做的就是减少链表要去执行的次数与要处理的数据
- Taro + Vue 中,使用 defineProperty 或 Proxy 的响应式进行更新,只需要知道减少对数据的设置频率和减少设置数据的大小,就可以同样起到优化 setData 的效果
# 三、页面管理
小程序框架管理了整个小程序的页面路由,可以做到页面间的无缝切换,同时还能管理页面完整的生命周期。
为了避免小程序内存过大的占用,小程序的页面栈被限制最多存在十层。
# 页面栈优化
1、通过wx.getCurrentPages
获取总栈数
2、页面跳转可以合理使用wx.redirectTo()
和wx.navigateBack
跳转,减少页面栈产生。比如:直播间 -> 主播页 -> 直播详情 -> 直播间 -> ...
3、页面返回首页时可以合理使用wx.reLaunch
移除无效页面栈
4、页面栈大于一定量时,通过wx.reLaunch
跳转,但是该方式无法再返回,因此需要针对特定页面去做
5、页面栈溢出时,强制使用wx.reLaunch
跳转,这里是降维保底方案
建议统一封装跳转方法,所有跳转统一维护,同时在进行页面栈移除时适当输出日志或提示
# 四、运行时
# 1、运行流程
# 2、运行机制
冷启动
用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动,这个过程称之为冷启动。
相关生命周期回调:onLaunch
、onLoad
、onReady
热启动
如果用户已经打开过小程序,然后在一定时间内再次打开该小程序时,无需重新冷启动,只需将后台的小程序切换到前台,这个过程称之为热启动。
相关生命周期回调:onShow
后台
小程序通过返回、胶囊按钮、home键、锁屏等方式就会进入后台。
相关生命周期回调:onHide
挂起
小程序进入后台状态一段时间后,微信会停止小程序 JS 线程的执行,保留内存状态,事件和接口回调会在小程序再次进入前台时触发。
销毁
小程序进入后台状态很长一段时间后,或者系统资源紧张,会完全终止运行。
相关生命周期回调:onSaveExitState
、onUnload
、onMemoryWarning
# 3、代码包大小优化
普通分包
引用原则:
- 主包无法引用分包内的私有资源
- 分包之间不能互相引用私有资源
- 分包可以引用主包内的公共资源
优点:
- 拆分总包,减少主包体积,使得加载速度得到提升
- 分包能共用主包资源
- 主包与分包之间状态共用
缺点:
- 分包不够彻底,对总包的体积减少不明显
- 分包的运行依赖主包
独立分包
引用原则:
- 独立分包、普通分包、主包之间是相互隔绝的,不能互相引用彼此的资源
- 独立分包和主包之间,不能互相引用私有资源,而普通分包可以
- 独立分包之间,不能互相引用私有资源
- 独立分包和普通分包之间,不能互相引用私有资源
优点:
- 减少分包体积,使得加载速度得到提升
- 每个分包都能独立运行
- 独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度
缺点:
- 分包之间无法共用资源,总包体积上升
- 独立分包不依赖主包即可运行,若部分功能依赖主包,会导致未知问题
- 公共状态难以处理
分包预加载
预下载分包行为,会再进入指定页面时触发,需要配合分包使用
分包异步化
在 web 中也叫按需加载,体积大的文件或组件运行时异步加载,能进一步减少主包与分包体积
组件异步加载
// pages/index.config.json
{
"usingComponents": {
"button": "../components/button",
"list": "../components/full-list",
"simple-list": "../components/simple-list"
},
"componentPlaceholder": {
"button": "view",
"list": "simple-list"
}
}
2
3
4
5
6
7
8
9
10
11
12
文件异步加载
// 使用回调函数风格的调用
require('../lib/sdk.js', sdk => {
console.log(sdk)
}, ({mod, errMsg}) => {
console.error(`path: ${mod}, ${errMsg}`)
})
// 或者使用 Promise 风格的调用
require.async('../lib/sdk.js').then(sdk => {
console.log(sdk)
}).catch(({mod, errMsg}) => {
console.error(`path: ${mod}, ${errMsg}`)
})
2
3
4
5
6
7
8
9
10
11
12
遗憾的是,在 Taro 编译时,使用的 require 是 webpack 的 require,这也就导致在 Taro 的小程序中,使用该方式尤为麻烦,需要注意以下几点:
- 需要通过 webpack 将异步加载的资源拆包出来
- 需要区分微信小程序与 webpack 的 require
- 需要注意按需加载文件的路径
# 4、渲染优化
骨架屏 在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。有以下方案:
- 微信开发者工具能自动生产骨架屏代码,该方案生成的文件是根据实际渲染的页面抽离的,代码质量一般,而且该方案并不适用 Taro,且需要额外处理
- 使用第三方框架,该方案可以自定义骨架屏模板,但是第三方的骨架屏不一定能满足需求
- 若是市面上的框架不满足,那就只能自定义骨架屏模板,需要额外投入成本
数据预拉取 在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度。
步骤:
- 微信后台设置下载地址
- 用户冷启动小程序,代码设置 token
- 用户再次冷启动小程序,检测到配置了预拉取
- 微信客户端调用下载地址获取数据并缓存整个 HTTP body
- 小程序通过回调拿到数据
App({
onLaunch() {
// 第一次打开小程序时会设置该 token,后续就能执行预拉取了
wx.setBackgroundFetchToken({
token: 'xxx'
})
wx.onBackgroundFetchData((res) => {
console.log(res.fetchedData) // 缓存数据
console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
})
wx.getBackgroundFetchData({
fetchType: 'pre',
success(res) {
console.log(res.fetchedData) // 缓存数据
console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
console.log(res.path) // 页面路径
console.log(res.query) // query 参数
console.log(res.scene) // 场景值
}
})
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
该方式在很大程度上可以优化冷启动首屏加载缓慢的问题,但是需要额外以下问题:
- token 有效期短会导致请求失败的问题,导致每次启动都是冷启动,配置无效
- token 有效期长可能会导致安全问题
数据周期性更新 在用户未打开小程序的情况下,微信客户端每隔 12 个小时会发起一次从服务器提前拉取数据的请求,当用户打开小程序时可以更快地渲染页面,减少用户等待时间
App({
onLaunch() {
wx.getBackgroundFetchData({
fetchType: 'periodic',
success(res) {
console.log(res.fetchedData) // 缓存数据
console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
}
})
}
})
2
3
4
5
6
7
8
9
10
11
该方式主要解决的问题是弱网或无网条件下的加载缓慢,而其存在的问题与数据预拉取相同
缓存请求数据
数据写入本地缓存,优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新
精简首屏数据
延迟请求非关键渲染数据,加快首屏渲染完成时间
避免阻塞渲染
慎用同步方法。如:wx.setStorageSync
,wx.getSystemInfoSync
# 5、后台页面
小程序通过wx.navigateTo
等方法跳转页面时,会留存上一个页面。因此在页面跳转时,需要清除页面中的定时器或事件监听;在不需要留存页面时使用wx.redirectTo
跳转
# 五、其他优化
# (1) UI交互优化
合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差。
安全区域设置
刘海屏、挖孔屏等设备需要预留安全区域,通过wx.getWindowInfo().safearea
获取
# (2) Taro 引用原生页面
在某些使用 Taro 不好处理的地方,可以通过直接引入原生模块或页面来编程
引入原生页面
// app.config.js
export default {
pages: ['pages/native/native'],
}
2
3
4
引用原生模块
// pages/index.config.ts
export default {
usingComponents: {
// 定义需要引入的第三方组件
// 1. key 值指定第三方组件名字,以小写开头
// 2. value 值指定第三方组件 js 文件的相对路径
'ec-canvas': '../../components/ec-canvas/ec-canvas',
},
}
2
3
4
5
6
7
8
9
引用的原生模块或页面,将不会再额外进行模块编译,该项目也就不能再跨端了。可以通过引用不同原生平台的代码来处理该问题。已知 iconfont 就是通过该方式实现了对小程序和 h5 的跨端。
# (3) 参考文档
- 微信小程序官方的 性能与体验 (opens new window)
- Taro 官方的 性能优化 (opens new window)
# 总结
Taro 的最大特性就是跨端,而它的短板则是在每个端都没法像原生小程序一样那么深入,在做对某端原生小程序的深入性能优化时,Taro 可能反而会成为阻碍。而 Taro 提供的原生混合方案,虽然一定程度能解决该问题,但是却失去了跨端的特性。
使用 Taro 开发小程序,隔离开了与小程序 API 的直接接触,从一定程度上来讲,无法感知编译后的代码在底层会做什么,会不会对性能产生影响。