Taro 助力京喜拼拼项目性能体验优化

做者:凹凸曼 - JJcss

背景

2020 年是社区团购风起云涌的一年,互联网大厂纷纷抓紧一分一秒跑步进场。“京喜拼拼”是京东旗下的社区团购平台,依托京东供应链体系,精选低价好货,为社区用户提供第二天达等优质服务。html

​京喜拼拼团队技术选型使用 Taro 以便于实现多端需求,所以 Taro 团队有幸参与到 “京喜拼拼” 小程序的性能体验优化工做。react

京喜拼拼

全面体验 - 梳理 Taro 写法最佳实践

咱们全面体验后和熟悉业务代码后梳理出一系列 Taro3 写法的最佳实践:git

1. 性能相关

对小程序的性能影响较大的有两个因素,分别是 setData数据量和单位时间 setData 函数的调用次数github

当遇到性能问题时,在项目中打印 setData 的数据将很是有利于帮助定位问题。开发者能够经过进入 Taro 项目的 dist/taro.js 文件,搜索定位 .setData 的调用位置,而后对数据进行打印。算法

在 Taro 中,会对 setDatabatch 捆绑更新操做,所以更多时候只须要考虑 setData 的数据量大小问题。小程序

如下是咱们梳理的开发者须要注意的写法问题,有一些问题须要开发者手动调整,一些问题 Taro 能够帮助自动化规避:api

1.1. 删除楼层节点须要谨慎处理

假设有一种这样一种结构:浏览器

<View>
  <!-- 轮播 -->
  <Slider />
  <!-- 商品组 -->
  <Goods />
  <!-- 模态弹窗 -->
  {isShowModal && <Modal />}
</View>
复制代码

Taro3 目前对节点的删除处理是有缺陷的。当 isShowModaltrue 变为 false 时,模态弹窗会从消失。此时 Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。性能优化

通常状况下,影响不会太大,开发者无须由此产生心智负担。但假若待删除节点的兄弟节点的 DOM 结构很是复杂,如一个个楼层组件,删除操做的反作用会致使 setData 数据量较大,从而影响性能。

解决办法:

目前咱们能够这样优化,隔离删除操做:

<View>
  <!-- 轮播 -->
  <Slider />
  <!-- 商品组 -->
  <Goods />
  <!-- 模态弹窗 -->
  <View> {isShowModal && <Modal />} </View>
</View>
复制代码

咱们正在对删除节点的算法进行优化,彻底规避这种没必要要的 setData,于 v3.1 推出。

1.2. 基础组件的属性尽可能保持引用

假设基础组件(如 ViewInput 等)的属性值为非基本类型时,尽可能保持对象的引用。

假设有如下写法:

<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}
/>
复制代码

1.3. 小程序基础组件尽可能不要挂载额外属性

基础组件(如 ViewInput 等)如若设置了非标准的属性,目前这些额外属性会被一并进行 setData,而实际上小程序并不会理会这些属性,因此 setData 的这部分数据是冗余的。

例如 Text 组件的标准属性有 selectableuser-selectspace decode 四个,若是咱们为它设置一个额外属性 something,那么这个额外的属性也是会被 setData。

<Text something='extra' />
复制代码

Taro v3.1 将会自动过滤这些额外属性,届时这个限制将再也不存在。

2. 体验相关

2.1. 滚动穿透

在小程序开发中,滑动蒙层弹窗等覆盖式元素时,滑动事件会冒泡到页面,使页面元素也跟着滑动,每每咱们的解决办法是设置 catchTouchMove 从而阻止冒泡。

因为 Taro3 事件机制的限制,小程序事件都以 bind 的形式进行绑定。因此和 Taro一、Taro2 不一样,调用 e.stopPropagation() 并不能阻止滚动穿透。

解决办法:
  1. 使用样式解决(推荐)

给须要禁用滚动的组件写一个样式,相似于:

{
  overflow:hidden;
  height: 100vh;
}
复制代码
  1. catchMove

对于 Map 等极个别组件,使用样式固定宽高也没法阻止滚动,由于这些组件自己就具备滚动的能力。因此第一种办法处理不了冒泡到 Map 组件上的滚动事件。

这时候能够为 View 组件增长 catchMove 属性:

// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />
复制代码

2.2. 跳转预加载

在小程序中,从调用 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)
复制代码

2.3. 建议把 Taro.getCurrentInstance() 的结果保存下来

开发中咱们经常会调用 Taro.getCurrentInstance() 获取小程序的 app、page 对象、路由参数等数据。但频繁调用它可能会致使问题。所以推荐把 Taro.getCurrentInstance() 的结果在组件中保存起来,以后直接使用:

class Index extends React.Component {
  inst = Taro.getCurrentInstance()

  componentDidMount () {
    console.log(this.inst)
  }
}
复制代码

难啃的骨头 - 购物车页

咱们在低端机上受到了性能的困扰,尤为是在购物车页面卡顿最为明显。经过分析页面结构和反思 Taro 底层实现,咱们主要采起了两项优化措施,提高了低端机型滚动的流畅度,同时将点击延时从 1.5s 降到 300ms。

1. 长列表优化

在 Taro3 中,咱们新增了虚拟列表这样一个特殊的组件,帮助不少社区的开发者对超长列表进行优化,相信不少同窗对虚拟列表的实现原理、包括下图都已是很熟悉了,但购物车页却给咱们提出了新的需求。

虚拟列表

1.1 不限制高度

虚拟列表根据 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和实际大小差异过大,在超长列表中会有较明显的问题,你们须要当心使用哦~

1.2 列表底部

列表的底部区域能够帮助咱们便捷地完成信息的展现,好比上拉加载等,对于虚拟列表也是如此。

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
+ }
}
复制代码

1.3 性能优化

在虚拟列表中,不管是使用那种布局方式,都会形成页面的回流,因此不论选择哪种对于浏览器内核渲染页面而言并无很大的区别。可是若是使用 relative,对于列表来讲,须要调整的节点样式要少得多。因此咱们在新的虚拟列表中也支持了这样的定位模式,供开发者自由选择。对于低端机型来讲,在咱们完成总体的渲染性能优化以前,relative 模式已经可以让虚拟列表在低端机型上拥有不错的体验。

2. 渲染性能优化

Taro3 使用小程序的 template 进行渲染,通常状况下并不会使用原生自定义组件。这会致使一个问题,全部的 setData 更新都是由页面对象调用,若是咱们的页面结构比较复杂,更新的性能就会降低。

层级过深时 setData 的数据结构:

page.setData({
  "root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
})
复制代码

针对这个问题,主要的思路是借用小程序的原生自定义组件,以达到局部更新的效果,从而提高更新性能。

指望的 setData 数据结构:

component.setData({
  "cn.[0].cn.[0].markers": []
})
复制代码

开发者有两种办法能够实现这个优化:

2.1 全局配置项 baseLevel

对于不支持模板递归的小程序(微信、QQ、京东小程序),在 DOM 层级达到必定数量后,Taro 会使用原生自定义组件协助递归。

简单理解就是 DOM 结构超过 N 层后,会使用原生自定义组件进行渲染。N 默认是 16 层,能够经过修改配置项 baseLevel 修改 N。

baseLevel 设置为 8 甚至 4 层,能很是有效地提高更新时的性能。可是设置是全局性的,会带来若干问题:

  1. flex 布局在跨原生自定义组件时会失效,这是影响最大的一个问题。
  2. SelectorQuery.select 方法的跨自定义组件的后代选择器写法须要增长 >>>.the-ancestor >>> .the-descendant

2.2 CustomWrapper 组件

为了解决全局配置不灵活的问题,咱们增长了一个基础组件 CustomWrapper。它的做用是建立一个原生自定义组件,对后代节点的 setData 将由此自定义组件进行调用,达到局部更新的效果。

开发者可使用它去包裹遇到更新性能问题的模块,提高更新时的性能。由于 CustomWrapper 组件须要手动使用,开发者可以清楚“这层使用了自定义组件,须要避免自定义组件的两个问题”。

例子
<CustomWrapper>
  <GoodsList> <Item /> <Item /> // ... </GoodsList>
</CustomWrapper>
复制代码

十全十美 - 体验评分平均 95+

把开发者工具的体验评分给拉满,这里咱们遇到了一个问题,开发者工具会识别全部绑定了点击事件的组件,若是组件的面积太小则提示点击区域太小,会影响“体验项”的评分。可是 Taro3 默认会为组件绑定上全部属性和事件。这样会“误伤”一些组件,它们虽然面积很小,实际上并无点击功能,但由于 Taro3 默认绑定的事件,被开发者工具认为点击区域太小,从而拉低体验评分。

Text 组件的模板,默认绑定了全部属性和事件:

<template name="tmpl_0_text">
  <text selectable="{{...}}" space="{{...}}" decode="{{...}}" user-select="{{...}}" style="{{...}}" class="{{...}}" id="{{...}}" bindtap="..." >
    ...
  </text>
</template>
复制代码

所以咱们为 ViewTextImage 组件各设立了一个 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 文本,不须要单独引入第三方自定义组件去进行解析,统一了多端标准。

Taro3 与原生项目混合

过去咱们对在 Taro 项目中混合使用原生的支持度较高。相反地,对在原生项目中混合使用 Taro 却没有过重视。可是市面上有着存量的原生开发小程序,他们接入 Taro 开发的改形成本每每很是大,最后只得放弃混合开发的想法。

通过本次项目,也驱使了咱们更加关注这部分需求,在 Taro v3.0.25 后推出了一套完整的原生项目混合使用 Taro 的方案

方案主要支持了三种场景:

  1. 在原生项目中使用 Taro 开发的页面。(已完成)
  2. 在原生项目的分包中运行完整的 Taro 项目。(已完成)
  3. 在原生项目中使用 Taro 开发的自定义组件。(正在开发中)

但愿以上方案能知足但愿逐步接入 Taro 的开发同窗。更多意见也欢迎在 Github 上给咱们留言。

尾声

Taro 团队此次参与到 “京喜拼拼” 小程序的性能体验优化工做,让咱们了解到 Taro3 的性能瓶颈所在,也体会到复杂业务的多样性。

2021 上半年咱们将更加聚焦于提高框架开发体验和运行性能、与原生小程序的混合,还有生态建设的工做上。

最后祝你们春节快乐~新的一年牛气冲天!

阅读原文

相关文章
相关标签/搜索