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 上半年咱們將更加聚焦於提高框架開發體驗和運行性能、與原生小程序的混合,還有生態建設的工做上。

最後祝你們春節快樂~新的一年牛氣沖天!

閱讀原文

相關文章
相關標籤/搜索