Taro | 高效能小程式的最佳實踐

張哥說技術發表於2023-12-21

來源:京東技術

導讀
Taro作為開放式跨端跨框架解決方案,在大量小程式和H5應用中得到廣泛實踐,本文將為讀者提供最佳實踐示例,以幫助最大程度地提升小程式應用的效能表現。




01 
前言


在今年的敏捷團隊建設中,我透過Suite執行器實現了一鍵自動化單元測試。Juint除了Suite執行器還有哪些執行器呢?由此我的Runner探索之旅開始了!
作為一個開放式的跨端跨框架解決方案,Taro 在大量的小程式和 H5 應用中得到了廣泛應用,同時也經常收到開發者的反饋,例如“渲染速度較慢”、“滑動不夠流暢”、“效能與原生應用相比有差距” 等。這表明效能問題一直是困擾開發者的一個重要問題。

熟悉 Taro 的開發者應該知道,相比於 Taro 1/2,Taro 3 是一個更加註重執行時而輕量化編譯時的框架。它的優勢在於提供了更高效的程式碼編寫方式,並擁有更豐富的生態系統。然而,這也意味著在效能方面可能會有一些損耗。


但是,使用 Taro 3 並不意味著必須犧牲應用的效能。事實上,Taro 已經提供了一系列的效能最佳化方法,並且不斷探索更加極致的最佳化方案。



02 
  

如何提升初次渲染效能

  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。
如果初次渲染的資料量非常大,可能會導致頁面在載入過程中出現一段時間的白屏,為了解決這個問題,Taro 提供了預渲染功能(Prerender)。

使用 Prerender 非常簡單,只需在專案根目錄下的 config 資料夾中找到 index.js/dev.js/prod.js 三者中的任意一個專案配置檔案,並根據專案情況進行修改。在編譯時,Taro CLI 會根據你的配置自動啟動預渲染功能。













const config = {  ...  mini: {    prerender: {      match: 'pages/shop/**', // 所有以 `pages/shop/` 開頭的頁面都參與 prerender      include: ['pages/any/way/index'], // `pages/any/way/index` 也會參與 prerender      exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用參與 prerender    }  }};
module.exports = config
更詳細說明請參考官方文件:


03 
  

如何提升更新效能

  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

由於 Taro 使用小程式的 template 進行渲染,這會引發一個問題:所有的 setData 更新都需要由頁面物件呼叫。當頁面結構較為複雜時,更新的效能可能會下降。

當層級過深時,setData 的資料結構如下:




page.setData({  'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],})

期望的 setData 資料結構:




component.setData({  'cn.[0].cn.[0].markers': [],})

目前有兩種方法可以實現上述結構,以實現區域性更新的效果,從而提升更新效能:

1. 全域性配置項 baseLevel

對於不支援模板遞迴的小程式(例如微信、QQ、京東小程式等),當 DOM 層級達到一定數量後,Taro 會利用原生自定義元件來輔助遞迴渲染。簡單來說,當 DOM 結構超過 N 層時,Taro 將使用原生自定義元件進行渲染(可以透過修改配置項 baseLevel 來調整 N 的值,建議設定為 8 或 4)。

需要注意的是,由於這是全域性設定,可能會帶來一些問題,例如:

在跨原生自定義元件時,flex 佈局會失效(這是影響最大的問題);

在 SelectorQuery.select 方法中,跨自定義元件的後代選擇器寫法需要增加 >>>:.the-ancestor >>> .the-descendant。

2. 使用 CustomWrapper 元件

CustomWrapper 元件的作用是建立一個原生自定義元件,用於呼叫後代節點的 setData 方法,以實現區域性更新的效果。

可以使用它來包裹那些遇到更新效能問題的模組,例如:













import { View, Text } from '@tarojs/components'
export default function () {  return (    <View className="index">      <Text>Demo</Text>      <CustomWrapper>        <GoodsList />      </CustomWrapper>    </View>  )}


04 
  如何提升長列表效能  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目

長列表是常見的元件,當生成或載入的資料量非常大時,可能會導致嚴重的效能問題,尤其在低端機上可能會出現明顯的卡頓現象。

為了解決長列表的問題,Taro 提供了 VirtualList 元件和 VirtualWaterfall 元件。它們的原理是隻渲染當前可見區域(Visible Viewport)的檢視,非可見區域的檢視在使用者滾動到可見區域時再進行渲染,以提高長列表滾動的流暢性。

Taro | 高效能小程式的最佳實踐

4.1  VirtualList 元件(虛擬列表)


    
以 React Like 框架使用為例,可以直接引入元件:

import VirtualList from '@tarojs/components/virtual-list'
一個最簡單的長列表元件如下所示:


































function buildData(offset = 0) {  return Array(100)    .fill(0)    .map((_, i) => i + offset)}
const Row = React.memo(({ id, index, data }) => {  return (    <View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>      Row {index} : {data[index]}    </View>  )})
export default class Index extends Component {  state = {    data: buildData(0),  }
 render() {    const { data } = this.state    const dataLen = data.length    return (      <VirtualList        height={800} /* 列表的高度 */        width="100%" /* 列表的寬度 */        item={Row} /* 列表單項元件,這裡只能傳入一個元件 */        itemData={data} /* 渲染列表的資料 */        itemCount={dataLen} /* 渲染列表的長度 */        itemSize={100} /* 列表單項的高度  */      />    )  }}
更多詳情可以參考官方文件:

4.2  VirtualWaterfall 元件(虛擬瀑布流)


    
以 React Like 框架使用為例,可以直接引入元件:

import { VirtualWaterfall } from `@tarojs/components-advanced`
一個最簡單的長列表元件如下所示:


































function buildData(offset = 0) {  return Array(100)    .fill(0)    .map((_, i) => i + offset)}
const Row = React.memo(({ id, index, data }) => {  return (    <View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>      Row {index} : {data[index]}    </View>  )})
export default class Index extends Component {  state = {    data: buildData(0),  }
 render() {    const { data } = this.state    const dataLen = data.length    return (      <VirtualWaterfall        height={800} /* 列表的高度 */        width="100%" /* 列表的寬度 */        item={Row} /* 列表單項元件,這裡只能傳入一個元件 */        itemData={data} /* 渲染列表的資料 */        itemCount={dataLen} /* 渲染列表的長度 */        itemSize={100} /* 列表單項的高度  */      />    )  }}
更多詳情以參考官方檔:ro-docs.jd.com/docs/virtual-waterfall


05
  

如何避免setData資料量較大

  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

眾所周知,對小程式效能的影響較大的主要有兩個因素,即 setData 的資料量和單位時間內呼叫 setData 函式的次數。在 Taro 中,會對 setData 進行批次更新操作,因此通常只需要關注 setData 的資料量大小。下面透過幾個例子來說明如何避免資料量過大的問題:

例子 1:刪除樓層節點要謹慎處理

目前 Taro 在處理節點刪除方面存在一些缺陷。假設存在以下程式碼寫法:









<View>  <!-- 輪播 -->  <Slider />  <!-- 商品組 -->  <Goods />  <!-- 模態彈窗 -->  {isShowModal && <Modal />}</View>

當 isShowModal 從 true 變為 false 時,模態彈窗會消失。此時,Modal 元件的兄弟節點都會被更新,setData 的資料是 Slider + Goods 元件的 DOM 節點資訊。

一般情況下,這不會對效能產生太大影響。然而,如果待刪除節點的兄弟節點的 DOM 結構非常複雜,比如一個個樓層元件,刪除操作的副作用會導致 setData 的資料量變大,從而影響效能。

為了解決這個問題,可以透過隔離刪除操作來進行最佳化。











<View>  <!-- 輪播 -->  <Slider />  <!-- 商品組 -->  <Goods />  <!-- 模態彈窗 -->  <View>    {isShowModal && <Modal />}  </View></View>

例子 2:基礎元件的屬性要保持引用

當基礎元件(例如 View、Input 等)的屬性值為非基本型別時,假設存在以下程式碼寫法:











<Map  latitude={22.53332}  longitude={113.93041}  markers={[    {      latitude: 22.53332,      longitude: 113.93041,    },  ]}/>

每次渲染時,React 會對基礎元件的屬性進行淺比較。如果發現 markers 的引用不同,就會觸發元件屬性的更新。這最終導致了 setData 操作的頻繁執行和資料量的增加。為了解決這個問題,可以使用狀態(state)或閉包等方法來保持物件的引用,從而避免不必要的更新。






<Map  latitude={22.53332}  longitude={113.93041}  markers={this.state.markers}/>


06 
  

更多最佳實踐

  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

6.1  阻止滾動穿透


    

在小程式開發中,當存在滑動蒙層、彈窗等覆蓋式元素時,滑動事件會冒泡到頁面上,導致頁面元素也會跟著滑動。通常我們會透過設定 catchTouchMove 來阻止事件冒泡。

然而,由於 Taro3 事件機制的限制,小程式事件都是以 bind 的形式進行繫結。因此,與 Taro1/2 不同,呼叫 e.stopPropagation() 並不能阻止滾動事件的穿透。

解決辦法 1:使用樣式(推薦)

可以為需要禁用滾動的元件編寫以下樣式:





{  overflow:hidden;  height: 100vh;}

解決辦法 2:使用 catchMove

對於極個別的元件,比如 Map 元件,即使使用樣式固定寬高也無法阻止滾動,因為這些元件本身具有滾動的功能。因此,第一種方法無法處理冒泡到 Map 元件上的滾動事件。在這種情況下,可以為 View 元件新增 catchMove 屬性:



// 這個 View 元件會繫結 catchtouchmove 事件而不是 bindtouchmove<View catchMove />

6.2  跳轉預載入


    

在小程式中,當呼叫 Taro.navigateTo 等跳轉類 API 後,新頁面的 onLoad 事件會有一定的延時。因此,為了提高使用者體驗,可以將一些操作(如網路請求)提前到呼叫跳轉 API 之前執行。

對於熟悉 Taro 的開發者來說,可能會記得在 Taro 1/2 中有一個名為 componentWillPreload 的鉤子函式。然而,在 Taro 3 中,這個鉤子函式已經被移除了。不過,開發者可以使用 Taro.preload() 方法來實現跳轉預載入的效果:




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


// pages/detail.jsconsole.log(getCurrentInstance().preloadData)

6.3  建議把Taro.getCurrentInstance() 的結果儲存下來


    

在開發過程中,我們經常會使用 Taro.getCurrentInstance() 方法來獲取小程式的 app、page 物件以及路由引數等資料。然而,頻繁地呼叫該方法可能會導致一些問題。

因此,建議將 Taro.getCurrentInstance() 的結果儲存在元件中,並在需要時直接使用,以避免頻繁呼叫該方法。這樣可以提高程式碼的執行效率和效能。








class Index extends React.Component {  inst = Taro.getCurrentInstance()
 componentDidMount() {    console.log(this.inst)  }}


07 
  

小程式編譯模式(CompileMode)

  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

Taro 一直追求並不斷突破效能的極限,除了以上提供的最佳實踐,Taro即將推出小程式編譯模式(CompileMode)。

什麼是 CompileMode?

前面已經說過,Taro3 是一種重執行時的框架,當節點數量增加到一定程度時,渲染效能會顯著下降。因此,為了解決這個問題,Taro 引入了 CompileMode 編譯模式。

CompileMode 在編譯階段對開發者的程式碼進行掃描,將 JSX 和 Vue template 程式碼提前編譯為相應的小程式模板程式碼。這樣可以減少小程式渲染層虛擬 DOM 樹節點的數量,從而提高渲染效能。透過使用 CompileMode,可以有效減少小程式的渲染負擔,提升應用的效能表現。

如何使用?

開發者只需為小程式的基礎元件新增 compileMode 屬性,該元件及其子元件將會被編譯為獨立的小程式模板。








function GoodsItem () {  return (    <View compileMode>      ...    </View>  )}

目前第一階段的開發工作已經完成,我們即將釋出 Beta 版本,歡迎大家關注!想提前瞭解的可以檢視 RFC 文件:



08
  結尾  


理解,首先 MCube 會依據模板快取狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為檢視樹的結構,轉換完成後將透過表示式引擎解析表示式並取得正確的值,透過事件解析引擎解析使用者自定義事件並完成事件的繫結,完成解析賦值以及事件繫結後進行檢視的渲染,最終將目標頁面展示到螢幕。

透過採用 Taro 的最佳實踐,相信您的小程式應用效能一定會有顯著的提升。未來,Taro將持續探索更多最佳化方案,覆蓋更廣泛的應用場景,為開發者提供更高效、更優秀的開發體驗。

如果在專案中有任何經驗總結或思考,歡迎交流分享,感謝支援!

Taro 開發文件:ro-docs.jd.com/docs/

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024923/viewspace-3000849/,如需轉載,請註明出處,否則將追究法律責任。

相關文章