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

Easter79
• 阅读 911

背景—

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

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

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

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

1. 性能相关

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

当遇到性能问题时,在项目中打印 setData 的数据将非常有利于帮助定位问题。开发者可以通过进入 Taro 项目的 dist/taro.js 文件,搜索定位 .setData 的调用位置,然后对数据进行打印。

在 Taro 中,会对 setDatabatch 捆绑更新操作,因此更多时候只需要考虑 setData 的数据量大小问题。

以下是我们梳理的开发者需要注意的写法问题,有一些问题需要开发者手动调整,一些问题 Taro 可以帮助自动化规避:

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-selectspacedecode 四个,如果我们为它设置一个额外属性 something,那么这个额外的属性也是会被 setData。

<Text something='extra' />

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

2. 体验相关

2.1. 滚动穿透

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

由于 Taro3 事件机制[1]的限制,小程序事件都以 bind 的形式进行绑定。所以和 Taro1、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 的同学可能会想起 Taro1、Taro2 中的 componentWillPreload 钩子。但 Taro3 不再提供这个钩子,开发者可以使用 Taro.preload() 方法实现跳转预加载:

// pages/index.jsTaro.preload(fetchSomething())Taro.navigateTo({ url: '/pages/detail' })

// pages/detail.jsconsole.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 中,我们新增了虚拟列表这样一个特殊的组件,帮助很多社区的开发者对超长列表进行优化,相信很多同学对虚拟列表的实现原理、包括下图都已经是很熟悉了,但购物车页却给我们提出了新的需求。

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

虚拟列表

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[2] 修改 N。

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

  1. flex 布局在跨原生自定义组件时会失效,这是影响最大的一个问题。

  2. SelectorQuery.select 方法的 跨自定义组件的后代选择器 [3]写法需要增加 >>>.the-ancestor >>> .the-descendant

2.2 CustomWrapper 组件

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

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

例子
<CustomWrapper>  <GoodsList>    <Item />    <Item />    // ...  </GoodsList></CustomWrapper>

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

把开发者工具的体验评分给拉满,这里我们遇到了一个问题,开发者工具会识别所有绑定了点击事件的组件,如果组件的面积过小则提示点击区域过小,会影响“体验项”的评分。但是 Taro3 默认会为组件绑定上所有属性和事件[4]。这样会“误伤”一些组件,它们虽然面积很小,实际上并没有点击功能,但因为 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>
优化后的购物车页体验评分

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

另一个战场 - 多端适配&原生混合—

适配京东小程序

适配京东小程序的过程比较顺利,需要改动的地方不多。

在此过程中 Taro3 最主要的升级是增强了对 HTML 文本的解析能力,增加了对 <style> 标签的支持。自此完全同步了 wxparse 的能力,开发者使用 React 的 dangerouslySetInnerHTML 或 Vue 的 v-html 即可很好地解析 HTML 文本,不需要单独引入第三方自定义组件去进行解析,统一了多端标准。

Taro3 与原生项目混合

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

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

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

  1. 在原生项目中使用 Taro 开发的页面。(已完成)

  2. 在原生项目的分包中运行完整的 Taro 项目。(已完成)

  3. 在原生项目中使用 Taro 开发的自定义组件。(正在开发中)

希望以上方案能满足希望逐步接入 Taro 的开发同学。更多意见也欢迎在 Github[6] 上给我们留言。

尾声—

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

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

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

参考资料

[1]

Taro3 事件机制: https://taro-docs.jd.com/taro/docs/react#%E9%98%BB%E6%AD%A2%E6%BB%9A%E5%8A%A8%E7%A9%BF%E9%80%8F

[2]

baseLevel: https://taro-docs.jd.com/taro/docs/next/config-detail#minibaselevel

[3]

跨自定义组件的后代选择器: https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.select.html

[4]

Taro3 默认会为组件绑定上所有属性和事件: https://taro-docs.jd.com/taro/docs/next/platform-plugin#%E5%B1%9E%E6%80%A7%E7%B2%BE%E7%AE%80

[5]

原生项目混合使用 Taro 的方案: https://taro-docs.jd.com/taro/docs/next/taro-in-miniapp

[6]

Github: https://github.com/NervJS/taro/issues

凹凸揭秘系列


  1. 凹凸实验室的过去与未来

  2. 凹凸技术揭秘·羚珑智能设计平台·逐梦设计数智化

  3. 凹凸技术揭秘 · Deco 智能代码 · 开启产研效率革命

  4. 凹凸技术揭秘·羚珑页面可视化·成长蜕变之路

  5. 凹凸技术揭秘 · 夸克设计资产 · 打造全矩阵优质物料

  6. 凹凸技术揭秘 · Tide 研发平台 · 布局研发新基建

  7. 凹凸技术揭秘 · Taro · 从跨端到开放式跨端跨框架

  8. 凹凸技术揭秘 · 基础服务体系 · 构筑服务端技术中枢

  9. 凹凸技术揭秘·技术精进与业务发展两不误

  10. 凹凸技术揭秘 · 数懒 · 增长团队好帮手

感谢一直关注凹凸实验室的读者,为了提供更优质的内容,希望您能抽出几分钟时间,完成一个小调查,明年凹凸公众号的内容由你决定。

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

加入凹凸实验室开放、开源、专业、有爱、有梦的大家庭?扫码下面的二维码了解职位。

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

还没有关注凹凸实验室的读者们,关注我们吧,我们一个月只有 4 次推送机会,我们很珍惜每一次推送,不会让你失望的。

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

本文分享自微信公众号 - 凹凸实验室(AOTULabs)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Swift之struct二进制大小分析
随着Swift的日渐成熟和给开发过程带来的便利性及安全性,京喜App中的原生业务模块和基础模块使用Swift开发占比逐渐增高。本次讨论的是struct对比Class的一些优劣势,重点分析对包体积带来的影响及规避措施。
Easter79 Easter79
3年前
Taro小程序自定义顶部导航栏
微信自带的顶部导航栏是无法支持自定义icon和增加元素的,在开发小程序的时候自带的根本满足不了需求,分享一个封装好的组件,支持自定义icon、扩展dom,适配安卓、ios、h5,全面屏。我用的是京东的Taro多端编译框架写的小程序,原生的也可以适用,用到的微信/taro的api做调整就行,实现效果如下。!在这里插入图片描述(https://i
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
京东云开发者 京东云开发者
11个月前
SPI扩展点在业务中的使用及原理分析 | 京东物流技术团队
目前仓储中台和京喜BP的合作主要通过SPI扩展点的方式。好处就是对修改封闭、对扩展开放,中台不需要关心BP的业务实现细节,通过对不同BP配置扩展点的接口来达到个性化的目的。目前京喜BP主要提供两种方式的接口实现,一种是jar包的方式,一种是提供jsf接口。
万字长文详解如何使用Swift提高代码质量 | 京东云技术团队
京喜APP最早在2019年引入了Swift,使用Swift完成了第一个订单模块的开发。之后一年多我们持续在团队/公司内部推广和普及Swift,目前Swift已经支撑了70%以上的业务。通过使用Swift提高了团队内同学的开发效率,同时也带来了质量的提升,目前来自Swift的Crash的占比不到1%。在这过程中不断的学习/实践,团队内的CodeReview,也对如何使用Swift来提高代码质量有更深的理解。
京喜APP - 图片库优化 | 京东云技术团队
京喜APP早期开发主要是快速原生化迭代替代原有H5,提高用户体验,在这期间也积累了不少性能问题。之后我们开始进行一些性能优化相关的工作,本文主要是介绍京喜图片库相关优化策略以及关于图片相关的一些关联知识。
Elasticsearch与Clickhouse数据存储对比 | 京东云技术团队
京喜达技术部在社区团购场景下采用JDQFlinkElasticsearch架构来打造实时数据报表。随着业务的发展Elasticsearch开始暴露出一些弊端,不适合大批量的数据查询,高频次分页导出导致宕机、存储成本较高。
京东到家小程序-在性能及多端能力的探索实践 | 京东云技术团队
随着京东到家小程序需要支持容器越来越多,每个端分开实现要面临研发成本高、不一致等问题,为了提高研发效率,经过技术选型采用了taro3原生混合开发模式,本文主要讲解我们是如何基于taro框架,进行多端能力的探索和性能优化
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k