做者:凹凸曼 - JJcss
2020 年是社区团购风起云涌的一年,互联网大厂纷纷抓紧一分一秒跑步进场。“京喜拼拼”是京东旗下的社区团购平台,依托京东供应链体系,精选低价好货,为社区用户提供第二天达等优质服务。html
京喜拼拼团队技术选型使用 Taro 以便于实现多端需求,所以 Taro 团队有幸参与到 “京喜拼拼” 小程序的性能体验优化工做。react
咱们全面体验后和熟悉业务代码后梳理出一系列 Taro3 写法的最佳实践:git
对小程序的性能影响较大的有两个因素,分别是 setData
的数据量和单位时间 setData
函数的调用次数。github
当遇到性能问题时,在项目中打印 setData
的数据将很是有利于帮助定位问题。开发者能够经过进入 Taro 项目的 dist/taro.js
文件,搜索定位 .setData
的调用位置,而后对数据进行打印。算法
在 Taro 中,会对 setData
作 batch 捆绑更新操做,所以更多时候只须要考虑 setData 的数据量大小问题。小程序
如下是咱们梳理的开发者须要注意的写法问题,有一些问题须要开发者手动调整,一些问题 Taro 能够帮助自动化规避:api
假设有一种这样一种结构:浏览器
<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>
复制代码
Taro3 目前对节点的删除处理是有缺陷的。当 isShowModal
由 true
变为 false
时,模态弹窗会从消失。此时 Modal
组件的兄弟节点都会被更新,setData
的数据是 Slider
+ Goods
组件的 DOM 节点信息。性能优化
通常状况下,影响不会太大,开发者无须由此产生心智负担。但假若待删除节点的兄弟节点的 DOM 结构很是复杂,如一个个楼层组件,删除操做的反作用会致使 setData
数据量较大,从而影响性能。
目前咱们能够这样优化,隔离删除操做:
<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View> {isShowModal && <Modal />} </View>
</View>
复制代码
咱们正在对删除节点的算法进行优化,彻底规避这种没必要要的 setData,于 v3.1 推出。
假设基础组件(如 View
、Input
等)的属性值为非基本类型时,尽可能保持对象的引用。
假设有如下写法:
<Map
latitude={22.53332}
longitude={113.93041}
markers={[{
latitude: 22.53332,
longitude: 113.93041
}]}
/>
复制代码
每次渲染时,React 会对基础组件的属性作浅对比,这时发现 markers
的引用不一样,就会去更新组件属性。最后致使 setData
次数增多、setData
数据量增大。
能够经过 state
、闭包等手段保持对象的引用:
<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>
复制代码
基础组件(如 View
、Input
等)如若设置了非标准的属性,目前这些额外属性会被一并进行 setData
,而实际上小程序并不会理会这些属性,因此 setData
的这部分数据是冗余的。
例如 Text
组件的标准属性有 selectable
、user-select
、space
、decode
四个,若是咱们为它设置一个额外属性 something
,那么这个额外的属性也是会被 setData。
<Text something='extra' />
复制代码
Taro v3.1 将会自动过滤这些额外属性,届时这个限制将再也不存在。
在小程序开发中,滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面,使页面元素也跟着滑动,每每咱们的解决办法是设置 catchTouchMove
从而阻止冒泡。
因为 Taro3 事件机制的限制,小程序事件都以 bind
的形式进行绑定。因此和 Taro一、Taro2 不一样,调用 e.stopPropagation()
并不能阻止滚动穿透。
给须要禁用滚动的组件写一个样式,相似于:
{
overflow:hidden;
height: 100vh;
}
复制代码
对于 Map
等极个别组件,使用样式固定宽高也没法阻止滚动,由于这些组件自己就具备滚动的能力。因此第一种办法处理不了冒泡到 Map
组件上的滚动事件。
这时候能够为 View 组件增长 catchMove 属性:
// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />
复制代码
在小程序中,从调用 Taro.navigateTo
等跳转类 API,到新页面触发 onLoad
会有必定延时。所以类如网络请求等操做能够提早到调用跳转 API 以前。
熟悉 Taro 的同窗可能会想起 Taro一、Taro2 中的 componentWillPreload
钩子。但 Taro3 再也不提供这个钩子,开发者可使用 Taro.preload()
方法实现跳转预加载:
// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })
复制代码
// pages/detail.js
console.log(getCurrentInstance().preloadData)
复制代码
开发中咱们经常会调用 Taro.getCurrentInstance()
获取小程序的 app、page 对象、路由参数等数据。但频繁调用它可能会致使问题。所以推荐把 Taro.getCurrentInstance()
的结果在组件中保存起来,以后直接使用:
class Index extends React.Component {
inst = Taro.getCurrentInstance()
componentDidMount () {
console.log(this.inst)
}
}
复制代码
咱们在低端机上受到了性能的困扰,尤为是在购物车页面卡顿最为明显。经过分析页面结构和反思 Taro 底层实现,咱们主要采起了两项优化措施,提高了低端机型滚动的流畅度,同时将点击延时从 1.5s 降到 300ms。
在 Taro3 中,咱们新增了虚拟列表这样一个特殊的组件,帮助不少社区的开发者对超长列表进行优化,相信不少同窗对虚拟列表的实现原理、包括下图都已是很熟悉了,但购物车页却给咱们提出了新的需求。
虚拟列表根据 itemSize
来计算每一个节点的位置,若是节点的宽高不肯定,在每一个节点至少加载完成一次以前,咱们很难去判断列表的真实尺寸。这也是为何在虚拟列表的早期版本中咱们并无支持这样的特性,而是选择固定了每一个节点的高度,避免让开发者使用虚拟列表时增长心智负担。
不过这个需求也并不是不能完成,简单地调整虚拟列表实现和使用的逻辑,咱们就能够轻松实现这个特性。
import VirtualList from `@tarojs/components/virtual-list`
function buildData (offset = 0) {
return Array(100).fill(0).map((_, i) => i + offset);
}
- const Row = React.memo(({ index, style, data }) => {
+ const Row = React.memo(({ id, index, style, data }) => {
return (
- <View className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
+ <View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
Row {index}
</View>
);
})
export default class Index extends Component {
state = {
data: buildData(0),
}
render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={500} // 列表的高度
width='100%' // 列表的宽度
itemData={data} // 渲染列表的数据
itemCount={dataLen} // 渲染列表的长度
itemSize={100} // 列表单项的高度
+ unlimitedSize={true} // 解开列表节点大小限制
>
{Row} // 列表单项组件,这里只能传入一个组件
</VirtualList>
);
}
}
复制代码
能够看到,咱们在新增了 id
传入来帮助获取每一个节点在首次加载以后读取它的真实大小,得益于 Taro 跨平台的优点,这是重构虚拟列表组件中最简单的一步,有了这个基础,咱们就能够将节点的实际大小和它们的位置信息关联到一块儿,让列表本身调整每一个节点的位置,并呈现给用户。
而对于开发者,若是想要使用这个模式,只须要传入 unlimitedSize
就可让虚拟列表解开高度限制。固然这并不意味着在使用虚拟列表时能够不须要传入节点大小, itemSize
在这个模式下将做为初始值辅助列表中每一个节点位置信息的计算。
若是
itemSize
和实际大小差异过大,在超长列表中会有较明显的问题,你们须要当心使用哦~
列表的底部区域能够帮助咱们便捷地完成信息的展现,好比上拉加载等,对于虚拟列表也是如此。
return (
<VirtualList
height={500} // 列表的高度
width='100%' // 列表的宽度
itemData={data} // 渲染列表的数据
itemCount={dataLen} // 渲染列表的长度
itemSize={100} // 列表单项的高度
+ renderBottom={<View>我就是底线</View>}
>
{Row} // 列表单项组件,这里只能传入一个组件
</VirtualList>
);
复制代码
固然也有同窗会注意到,在 虚拟列表 文档中是经过 scrollOffset > ((dataLen - 5) * itemSize + 100)
这样的方法来判断是否触底,这是由于咱们并无在 VirtualList
中返回滚动的详细信息,此次咱们也返回相关的数据,帮助你们更好地使用虚拟列表。
interface VirtualListEvent<T> {
/** 滚动方向,可能值为 forward 往前, backward 日后。 */
scrollDirection: 'forward' | 'backward'
/** 滚动距离 */
scrollOffset: number
/** 当滚动是由 scrollTo() 或 scrollToItem() 调用时返回 true,不然返回 false */
scrollUpdateWasRequested: boolean
/** 当前只有 React 支持 */
+ detail?: {
+ scrollLeft: number
+ scrollTop: number
+ scrollHeight: number
+ scrollWidth: number
+ clientWidth: number
+ clientHeight: number
+ }
}
复制代码
在虚拟列表中,不管是使用那种布局方式,都会形成页面的回流,因此不论选择哪种对于浏览器内核渲染页面而言并无很大的区别。可是若是使用 relative
,对于列表来讲,须要调整的节点样式要少得多。因此咱们在新的虚拟列表中也支持了这样的定位模式,供开发者自由选择。对于低端机型来讲,在咱们完成总体的渲染性能优化以前,relative
模式已经可以让虚拟列表在低端机型上拥有不错的体验。
Taro3 使用小程序的 template
进行渲染,通常状况下并不会使用原生自定义组件。这会致使一个问题,全部的 setData
更新都是由页面对象调用,若是咱们的页面结构比较复杂,更新的性能就会降低。
层级过深时 setData
的数据结构:
page.setData({
"root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
})
复制代码
针对这个问题,主要的思路是借用小程序的原生自定义组件,以达到局部更新的效果,从而提高更新性能。
指望的 setData
数据结构:
component.setData({
"cn.[0].cn.[0].markers": []
})
复制代码
开发者有两种办法能够实现这个优化:
对于不支持模板递归的小程序(微信、QQ、京东小程序),在 DOM 层级达到必定数量后,Taro 会使用原生自定义组件协助递归。
简单理解就是 DOM 结构超过 N 层后,会使用原生自定义组件进行渲染。N 默认是 16 层,能够经过修改配置项 baseLevel 修改 N。
把 baseLevel
设置为 8
甚至 4
层,能很是有效地提高更新时的性能。可是设置是全局性的,会带来若干问题:
flex
布局在跨原生自定义组件时会失效,这是影响最大的一个问题。SelectorQuery.select
方法的跨自定义组件的后代选择器写法须要增长 >>>
:.the-ancestor >>> .the-descendant
为了解决全局配置不灵活的问题,咱们增长了一个基础组件 CustomWrapper
。它的做用是建立一个原生自定义组件,对后代节点的 setData
将由此自定义组件进行调用,达到局部更新的效果。
开发者可使用它去包裹遇到更新性能问题的模块,提高更新时的性能。由于 CustomWrapper
组件须要手动使用,开发者可以清楚“这层使用了自定义组件,须要避免自定义组件的两个问题”。
<CustomWrapper>
<GoodsList> <Item /> <Item /> // ... </GoodsList>
</CustomWrapper>
复制代码
把开发者工具的体验评分给拉满,这里咱们遇到了一个问题,开发者工具会识别全部绑定了点击事件的组件,若是组件的面积太小则提示点击区域太小,会影响“体验项”的评分。可是 Taro3 默认会为组件绑定上全部属性和事件。这样会“误伤”一些组件,它们虽然面积很小,实际上并无点击功能,但由于 Taro3 默认绑定的事件,被开发者工具认为点击区域太小,从而拉低体验评分。
Text
组件的模板,默认绑定了全部属性和事件:
<template name="tmpl_0_text">
<text selectable="{{...}}" space="{{...}}" decode="{{...}}" user-select="{{...}}" style="{{...}}" class="{{...}}" id="{{...}}" bindtap="..." >
...
</text>
</template>
复制代码
所以咱们为 View
、Text
、Image
组件各设立了一个 static 模板,当检测到组件没有绑定事件时,则使用 static
模板,避免被“误伤”。
另外一方面,这一举动也能减小小程序 DOM 绑定的事件,对性能稍有提高,并且减小了属性让开发者工具的 xml
面板在调试时更加清晰。但这一方案也存在瑕疵,会致使编译后的 base.wxml
体积略微增大,和性能权衡来看,这仍然是值得的。
Text
组件的 static
模板,没有绑定事件:
<template name="tmpl_0_static-text">
<text selectable="{{...}}" space="{{...}}" decode="{{...}}" user-select="{{...}}" style="{{...}}" class="{{...}}" id="{{...}}" >
...
</text>
</template>
复制代码
以一锅羊蝎子结束了支援之旅后,咱们终于迎来了南方的艳阳天。但工做还没结束,仍有两项工做须要跟进。
适配京东小程序的过程比较顺利,须要改动的地方很少。
在此过程当中 Taro3 最主要的升级是加强了对 HTML 文本的解析能力,增长了对 <style>
标签的支持。自此彻底同步了 wxparse
的能力,开发者使用 React 的 dangerouslySetInnerHTML
或 Vue 的 v-html
便可很好地解析 HTML 文本,不须要单独引入第三方自定义组件去进行解析,统一了多端标准。
过去咱们对在 Taro 项目中混合使用原生的支持度较高。相反地,对在原生项目中混合使用 Taro 却没有过重视。可是市面上有着存量的原生开发小程序,他们接入 Taro 开发的改形成本每每很是大,最后只得放弃混合开发的想法。
通过本次项目,也驱使了咱们更加关注这部分需求,在 Taro v3.0.25 后推出了一套完整的原生项目混合使用 Taro 的方案。
方案主要支持了三种场景:
但愿以上方案能知足但愿逐步接入 Taro 的开发同窗。更多意见也欢迎在 Github 上给咱们留言。
Taro 团队此次参与到 “京喜拼拼” 小程序的性能体验优化工做,让咱们了解到 Taro3 的性能瓶颈所在,也体会到复杂业务的多样性。
2021 上半年咱们将更加聚焦于提高框架开发体验和运行性能、与原生小程序的混合,还有生态建设的工做上。
最后祝你们春节快乐~新的一年牛气冲天!