Taro 助力京喜拼拼項目性能體驗優(yōu)化
背景—
2020 年是社區(qū)團(tuán)購風(fēng)起云涌的一年,互聯(lián)網(wǎng)大廠紛紛抓緊一分一秒跑步進(jìn)場?!熬┫财雌础?微信搜京喜拼拼)是京東旗下的社區(qū)團(tuán)購平臺,依托京東供應(yīng)鏈體系,精選低價好貨,為社區(qū)用戶提供次日達(dá)等優(yōu)質(zhì)服務(wù)。
京喜拼拼團(tuán)隊技術(shù)選型使用 Taro 以便于實現(xiàn)多端需求,因此 Taro 團(tuán)隊有幸參與到 “京喜拼拼” 小程序的性能體驗優(yōu)化工作。
全面體驗 - 梳理 Taro 寫法最佳實踐—
我們?nèi)骟w驗后和熟悉業(yè)務(wù)代碼后梳理出一系列 Taro3 寫法的最佳實踐:
1. 性能相關(guān)
對小程序的性能影響較大的有兩個因素,分別是 setData 的數(shù)據(jù)量和單位時間 setData 函數(shù)的調(diào)用次數(shù)。
當(dāng)遇到性能問題時,在項目中打印 setData 的數(shù)據(jù)將非常有利于幫助定位問題。開發(fā)者可以通過進(jìn)入 Taro 項目的 dist/taro.js 文件,搜索定位 .setData 的調(diào)用位置,然后對數(shù)據(jù)進(jìn)行打印。
在 Taro 中,會對 setData 做 batch 捆綁更新操作,因此更多時候只需要考慮 setData 的數(shù)據(jù)量大小問題。
以下是我們梳理的開發(fā)者需要注意的寫法問題,有一些問題需要開發(fā)者手動調(diào)整,一些問題 Taro 可以幫助自動化規(guī)避:
1.1. 刪除樓層節(jié)點需要謹(jǐn)慎處理
假設(shè)有一種這樣一種結(jié)構(gòu):
<Slider />
<Goods />
{isShowModal && <Modal />}
</View>
Taro3 目前對節(jié)點的刪除處理是有缺陷的。當(dāng) isShowModal 由 true 變?yōu)?false 時,模態(tài)彈窗會從消失。此時 Modal 組件的兄弟節(jié)點都會被更新,setData 的數(shù)據(jù)是 Slider + Goods 組件的 DOM 節(jié)點信息。
一般情況下,影響不會太大,開發(fā)者無須由此產(chǎn)生心智負(fù)擔(dān)。但倘若待刪除節(jié)點的兄弟節(jié)點的 DOM 結(jié)構(gòu)非常復(fù)雜,如一個個樓層組件,刪除操作的副作用會導(dǎo)致 setData 數(shù)據(jù)量較大,從而影響性能。
解決辦法:
目前我們可以這樣優(yōu)化,隔離刪除操作:
<Slider />
<Goods />
<View>
{isShowModal && <Modal />}
View>
</View>
我們正在對刪除節(jié)點的算法進(jìn)行優(yōu)化,完全規(guī)避這種不必要的 setData,于 v3.1 推出。
1.2. 基礎(chǔ)組件的屬性盡量保持引用
假設(shè)基礎(chǔ)組件(如 View、Input 等)的屬性值為非基本類型時,盡量保持對象的引用。
假設(shè)有以下寫法:
<Map
latitude={22.53332}
longitude={113.93041}
markers={[{
latitude: 22.53332,
longitude: 113.93041
}]}
/>
每次渲染時,React 會對基礎(chǔ)組件的屬性做淺對比,這時發(fā)現(xiàn) markers 的引用不同,就會去更新組件屬性。最后導(dǎo)致 setData 次數(shù)增多、setData 數(shù)據(jù)量增大。
解決辦法:
可以通過 state、閉包等手段保持對象的引用:
<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>
1.3. 小程序基礎(chǔ)組件盡量不要掛載額外屬性
基礎(chǔ)組件(如 View、Input 等)如若設(shè)置了非標(biāo)準(zhǔn)的屬性,目前這些額外屬性會被一并進(jìn)行 setData,而實際上小程序并不會理會這些屬性,所以 setData 的這部分?jǐn)?shù)據(jù)是冗余的。
例如 Text 組件的標(biāo)準(zhǔn)屬性有 selectable、user-select、space、decode 四個,如果我們?yōu)樗O(shè)置一個額外屬性 something,那么這個額外的屬性也是會被 setData。
'extra' />
Taro v3.1 將會自動過濾這些額外屬性,屆時這個限制將不再存在。
2. 體驗相關(guān)
2.1. 滾動穿透
在小程序開發(fā)中,滑動蒙層、彈窗等覆蓋式元素時,滑動事件會冒泡到頁面,使頁面元素也跟著滑動,往往我們的解決辦法是設(shè)置 catchTouchMove 從而阻止冒泡。
由于 Taro3 事件機制[1]的限制,小程序事件都以 bind 的形式進(jìn)行綁定。所以和 Taro1、Taro2 不同,調(diào)用 e.stopPropagation() 并不能阻止?jié)L動穿透。
解決辦法:
使用樣式解決(推薦)
給需要禁用滾動的組件寫一個樣式,類似于:
{
overflow:hidden;
height: 100vh;
}
catchMove
對于 Map 等極個別組件,使用樣式固定寬高也無法阻止?jié)L動,因為這些組件本身就具有滾動的能力。所以第一種辦法處理不了冒泡到 Map 組件上的滾動事件。
這時候可以為 View 組件增加 catchMove 屬性:
// 這個 View 組件會綁定 catchtouchmove 事件而不是 bindtouchmove
2.2. 跳轉(zhuǎn)預(yù)加載
在小程序中,從調(diào)用 Taro.navigateTo 等跳轉(zhuǎn)類 API,到新頁面觸發(fā) onLoad 會有一定延時。因此類如網(wǎng)絡(luò)請求等操作可以提前到調(diào)用跳轉(zhuǎn) API 之前。
熟悉 Taro 的同學(xué)可能會想起 Taro1、Taro2 中的 componentWillPreload 鉤子。但 Taro3 不再提供這個鉤子,開發(fā)者可以使用 Taro.preload() 方法實現(xiàn)跳轉(zhuǎn)預(yù)加載:
// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })
// pages/detail.js
console.log(getCurrentInstance().preloadData)
2.3. 建議把 Taro.getCurrentInstance() 的結(jié)果保存下來
開發(fā)中我們常常會調(diào)用 Taro.getCurrentInstance() 獲取小程序的 app、page 對象、路由參數(shù)等數(shù)據(jù)。但頻繁調(diào)用它可能會導(dǎo)致問題。因此推薦把 Taro.getCurrentInstance() 的結(jié)果在組件中保存起來,之后直接使用:
class Index extends React.Component {
inst = Taro.getCurrentInstance()
componentDidMount () {
console.log(this.inst)
}
}
難啃的骨頭 - 購物車頁—
我們在低端機上受到了性能的困擾,尤其是在購物車頁面卡頓最為明顯。通過分析頁面結(jié)構(gòu)和反思 Taro 底層實現(xiàn),我們主要采取了兩項優(yōu)化措施,提升了低端機型滾動的流暢度,同時將點擊延時從 1.5s 降到 300ms。
1. 長列表優(yōu)化
在 Taro3 中,我們新增了虛擬列表這樣一個特殊的組件,幫助很多社區(qū)的開發(fā)者對超長列表進(jìn)行優(yōu)化,相信很多同學(xué)對虛擬列表的實現(xiàn)原理、包括下圖都已經(jīng)是很熟悉了,但購物車頁卻給我們提出了新的需求。

1.1 不限制高度
虛擬列表根據(jù) itemSize 來計算每個節(jié)點的位置,如果節(jié)點的寬高不確定,在每個節(jié)點至少加載完成一次之前,我們很難去判斷列表的真實尺寸。這也是為什么在虛擬列表的早期版本中我們并沒有支持這樣的特性,而是選擇固定了每個節(jié)點的高度,避免讓開發(fā)者使用虛擬列表時增加心智負(fù)擔(dān)。
不過這個需求也并非不能完成,簡單地調(diào)整虛擬列表實現(xiàn)和使用的邏輯,我們就可以輕松實現(xiàn)這個特性。
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 (
-
+
Row {index}
);
})
export default class Index extends Component {
state = {
data: buildData(0),
}
render() {
const { data } = this.state
const dataLen = data.length
return (
height={500} // 列表的高度
width='100%' // 列表的寬度
itemData={data} // 渲染列表的數(shù)據(jù)
itemCount={dataLen} // 渲染列表的長度
itemSize={100} // 列表單項的高度
+ unlimitedSize={true} // 解開列表節(jié)點大小限制
>
{Row} // 列表單項組件,這里只能傳入一個組件
);
}
}
可以看到,我們在新增了 id 傳入來幫助獲取每個節(jié)點在首次加載之后讀取它的真實大小,得益于 Taro 跨平臺的優(yōu)勢,這是重構(gòu)虛擬列表組件中最簡單的一步,有了這個基礎(chǔ),我們就可以將節(jié)點的實際大小和它們的位置信息關(guān)聯(lián)到一起,讓列表自己調(diào)整每個節(jié)點的位置,并呈現(xiàn)給用戶。
而對于開發(fā)者,如果想要使用這個模式,只需要傳入 unlimitedSize 就可以讓虛擬列表解開高度限制。當(dāng)然這并不意味著在使用虛擬列表時可以不需要傳入節(jié)點大小, itemSize 在這個模式下將作為初始值輔助列表中每個節(jié)點位置信息的計算。
如果
itemSize和實際大小差別過大,在超長列表中會有較明顯的問題,大家需要小心使用哦~
1.2 列表底部
列表的底部區(qū)域可以幫助我們便捷地完成信息的展示,比如上拉加載等,對于虛擬列表也是如此。
return (
height={500} // 列表的高度
width='100%' // 列表的寬度
itemData={data} // 渲染列表的數(shù)據(jù)
itemCount={dataLen} // 渲染列表的長度
itemSize={100} // 列表單項的高度
+ renderBottom={我就是底線 }
>
{Row} // 列表單項組件,這里只能傳入一個組件
);
當(dāng)然也有同學(xué)會注意到,在 虛擬列表 文檔中是通過 scrollOffset > ((dataLen - 5) * itemSize + 100) 這樣的方法來判斷是否觸底,這是因為我們并沒有在 VirtualList 中返回滾動的詳細(xì)信息,這次我們也返回相關(guān)的數(shù)據(jù),幫助大家更好地使用虛擬列表。
interface VirtualListEvent {
/** 滾動方向,可能值為 forward 往前, backward 往后。*/
scrollDirection: 'forward' | 'backward'
/** 滾動距離 */
scrollOffset: number
/** 當(dāng)滾動是由 scrollTo() 或 scrollToItem() 調(diào)用時返回 true,否則返回 false */
scrollUpdateWasRequested: boolean
/** 當(dāng)前只有 React 支持 */
+ detail?: {
+ scrollLeft: number
+ scrollTop: number
+ scrollHeight: number
+ scrollWidth: number
+ clientWidth: number
+ clientHeight: number
+ }
}
1.3 性能優(yōu)化
在虛擬列表中,無論是使用那種布局方式,都會造成頁面的回流,所以不論選擇哪一種對于瀏覽器內(nèi)核渲染頁面而言并沒有很大的區(qū)別。但是如果使用 relative,對于列表來說,需要調(diào)整的節(jié)點樣式要少得多。所以我們在新的虛擬列表中也支持了這樣的定位模式,供開發(fā)者自由選擇。對于低端機型來說,在我們完成整體的渲染性能優(yōu)化之前,relative 模式已經(jīng)能夠讓虛擬列表在低端機型上擁有不錯的體驗。
2. 渲染性能優(yōu)化
Taro3 使用小程序的 template 進(jìn)行渲染,一般情況下并不會使用原生自定義組件。這會導(dǎo)致一個問題,所有的 setData 更新都是由頁面對象調(diào)用,如果我們的頁面結(jié)構(gòu)比較復(fù)雜,更新的性能就會下降。
層級過深時 setData 的數(shù)據(jù)結(jié)構(gòu):
page.setData({
"root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
})
針對這個問題,主要的思路是借用小程序的原生自定義組件,以達(dá)到局部更新的效果,從而提升更新性能。
期望的 setData 數(shù)據(jù)結(jié)構(gòu):
component.setData({
"cn.[0].cn.[0].markers": []
})
開發(fā)者有兩種辦法可以實現(xiàn)這個優(yōu)化:
2.1 全局配置項 baseLevel
對于不支持模板遞歸的小程序(微信、QQ、京東小程序),在 DOM 層級達(dá)到一定數(shù)量后,Taro 會使用原生自定義組件協(xié)助遞歸。
簡單理解就是 DOM 結(jié)構(gòu)超過 N 層后,會使用原生自定義組件進(jìn)行渲染。N 默認(rèn)是 16 層,可以通過修改配置項 baseLevel[2] 修改 N。
把 baseLevel 設(shè)置為 8 甚至 4 層,能非常有效地提升更新時的性能。但是設(shè)置是全局性的,會帶來若干問題:
flex布局在跨原生自定義組件時會失效,這是影響最大的一個問題。SelectorQuery.select方法的跨自定義組件的后代選擇器[3]寫法需要增加>>>:.the-ancestor >>> .the-descendant
2.2 CustomWrapper 組件
為了解決全局配置不靈活的問題,我們增加了一個基礎(chǔ)組件 CustomWrapper。它的作用是創(chuàng)建一個原生自定義組件,對后代節(jié)點的 setData 將由此自定義組件進(jìn)行調(diào)用,達(dá)到局部更新的效果。
開發(fā)者可以使用它去包裹遇到更新性能問題的模塊,提升更新時的性能。因為 CustomWrapper 組件需要手動使用,開發(fā)者能夠清楚“這層使用了自定義組件,需要避免自定義組件的兩個問題”。
例子
<GoodsList>
<Item />
<Item />
// ...
GoodsList>
</CustomWrapper>
十全十美 - 體驗評分平均 95+—
把開發(fā)者工具的體驗評分給拉滿,這里我們遇到了一個問題,開發(fā)者工具會識別所有綁定了點擊事件的組件,如果組件的面積過小則提示點擊區(qū)域過小,會影響“體驗項”的評分。但是 Taro3 默認(rèn)會為組件綁定上所有屬性和事件[4]。這樣會“誤傷”一些組件,它們雖然面積很小,實際上并沒有點擊功能,但因為 Taro3 默認(rèn)綁定的事件,被開發(fā)者工具認(rèn)為點擊區(qū)域過小,從而拉低體驗評分。
Text 組件的模板,默認(rèn)綁定了所有屬性和事件:
<template name="tmpl_0_text">
<text
selectable="{{...}}"
space="{{...}}"
decode="{{...}}"
user-select="{{...}}"
style="{{...}}"
class="{{...}}"
id="{{...}}"
bindtap="..."
>
...
text>
template>
因此我們?yōu)?View、Text、Image 組件各設(shè)立了一個 static 模板,當(dāng)檢測到組件沒有綁定事件時,則使用 static 模板,避免被“誤傷”。
另一方面,這一舉動也能減少小程序 DOM 綁定的事件,對性能稍有提升,而且減少了屬性讓開發(fā)者工具的 xml 面板在調(diào)試時更加清晰。但這一方案也存在瑕疵,會導(dǎo)致編譯后的 base.wxml 體積略微增大,和性能權(quán)衡來看,這仍然是值得的。
Text 組件的 static 模板,沒有綁定事件:
<template name="tmpl_0_static-text">
<text
selectable="{{...}}"
space="{{...}}"
decode="{{...}}"
user-select="{{...}}"
style="{{...}}"
class="{{...}}"
id="{{...}}"
>
...
text>
template>
優(yōu)化后的購物車頁體驗評分

另一個戰(zhàn)場 - 多端適配&原生混合—
適配京東小程序
適配京東小程序的過程比較順利,需要改動的地方不多。
在此過程中 Taro3 最主要的升級是增強了對 HTML 文本的解析能力,增加了對 標(biāo)簽的支持。自此完全同步了 wxparse 的能力,開發(fā)者使用 React 的 dangerouslySetInnerHTML 或 Vue 的 v-html 即可很好地解析 HTML 文本,不需要單獨引入第三方自定義組件去進(jìn)行解析,統(tǒng)一了多端標(biāo)準(zhǔn)。
Taro3 與原生項目混合
過去我們對在 Taro 項目中混合使用原生的支持度較高。相反地,對在原生項目中混合使用 Taro 卻沒有太重視。但是市面上有著存量的原生開發(fā)小程序,他們接入 Taro 開發(fā)的改造成本往往非常大,最后只得放棄混合開發(fā)的想法。
經(jīng)過本次項目,也驅(qū)使了我們更加關(guān)注這部分需求,在 Taro v3.0.25 后推出了一套完整的原生項目混合使用 Taro 的方案[5]。
方案主要支持了三種場景:
在原生項目中使用 Taro 開發(fā)的頁面。(已完成) 在原生項目的分包中運行完整的 Taro 項目。(已完成) 在原生項目中使用 Taro 開發(fā)的自定義組件。(正在開發(fā)中)
希望以上方案能滿足希望逐步接入 Taro 的開發(fā)同學(xué)。更多意見也歡迎在 Github[6] 上給我們留言。
尾聲—
Taro 團(tuán)隊這次參與到 “京喜拼拼” 小程序的性能體驗優(yōu)化工作,讓我們了解到 Taro3 的性能瓶頸所在,也體會到復(fù)雜業(yè)務(wù)的多樣性。
2021 上半年我們將更加聚焦于提升框架開發(fā)體驗和運行性能、與原生小程序的混合,還有生態(tài)建設(shè)的工作上。
最后祝大家春節(jié)快樂~新的一年牛氣沖天!
參考資料
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 默認(rèn)會為組件綁定上所有屬性和事件: 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
凹凸揭秘系列
—
最后
歡迎加我微信(winty230),拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...



