自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Taro性能優(yōu)化之復(fù)雜列表篇

移動(dòng)開發(fā)
本文將以復(fù)雜列表的性能優(yōu)化為主旨,嘗試建立檢測(cè)指標(biāo),了解性能瓶頸,通過(guò)預(yù)加載、緩存、優(yōu)化組件層級(jí)、優(yōu)化數(shù)據(jù)結(jié)構(gòu)等多種方式,實(shí)驗(yàn)后提供一些技術(shù)方案的建議,希望可以給大家?guī)?lái)一些思路。
作者 | Kenny,攜程高級(jí)前端開發(fā)工程師。2021年加入攜程,從事小程序/H5相關(guān)研發(fā)工作。

一、背景

隨著項(xiàng)目的不斷迭代,規(guī)模日益增大,而基于Taro3的運(yùn)行時(shí)弊端也日漸凸顯,尤其在復(fù)雜列表頁(yè)面上表現(xiàn)欠佳,極度影響用戶體驗(yàn)。本文將以復(fù)雜列表的性能優(yōu)化為主旨,嘗試建立檢測(cè)指標(biāo),了解性能瓶頸,通過(guò)預(yù)加載、緩存、優(yōu)化組件層級(jí)、優(yōu)化數(shù)據(jù)結(jié)構(gòu)等多種方式,實(shí)驗(yàn)后提供一些技術(shù)方案的建議,希望可以給大家?guī)?lái)一些思路。

二、問(wèn)題現(xiàn)狀及分析

我們以酒店某一多功能列表為例(下圖),設(shè)定檢測(cè)標(biāo)準(zhǔn)(setData次數(shù)及該setData的響應(yīng)時(shí)效作為指標(biāo)),檢測(cè)情況如下:

圖片

圖片

指標(biāo)

setData次數(shù)

渲染耗時(shí)(ms)

第一次進(jìn)入列表頁(yè)

7

2404

下拉長(zhǎng)列表更新

3

1903

多屏列表下 篩選項(xiàng)更新

2

1758

多屏列表下 列表項(xiàng)更新

2

748

由于歷史原因,該頁(yè)面的代碼,由微信的原生轉(zhuǎn)成的taro1,后續(xù)迭代至taro3。項(xiàng)目中存在小程序原生寫法可能忽略的問(wèn)題。根據(jù)上面多次測(cè)出的指標(biāo)值,以及視覺(jué)體驗(yàn)上來(lái)看,存在以下問(wèn)題:

2.1  首次進(jìn)入列表頁(yè)的加載時(shí)間過(guò)長(zhǎng),白屏?xí)r間久

  • 列表頁(yè)請(qǐng)求的接口時(shí)間過(guò)長(zhǎng);
  • 初始化列表也是setData數(shù)據(jù)量過(guò)大,且次數(shù)過(guò)多;
  • 頁(yè)面節(jié)點(diǎn)數(shù)過(guò)多,導(dǎo)致渲染耗時(shí)較長(zhǎng);

2.2  頁(yè)面篩選項(xiàng)的更新卡頓,下拉動(dòng)畫卡頓

  • 篩選項(xiàng)中節(jié)點(diǎn)過(guò)多,更新時(shí)setData數(shù)據(jù)量大;
  • 篩選項(xiàng)的組件更新會(huì)導(dǎo)致頁(yè)面跟著一起更新;

2.3  無(wú)限列表的更新卡頓,滑動(dòng)過(guò)快會(huì)白屏

  • 請(qǐng)求下一頁(yè)的時(shí)機(jī)過(guò)晚;
  • setData時(shí)數(shù)據(jù)量大,響應(yīng)慢;
  • 滑動(dòng)過(guò)快時(shí),沒(méi)有從白屏到渲染完成的過(guò)渡機(jī)制,體驗(yàn)欠佳;

三、嘗試優(yōu)化的方案

3.1  跳轉(zhuǎn)預(yù)加載API:

通過(guò)觀察小程序的請(qǐng)求可以發(fā)現(xiàn),列表頁(yè)請(qǐng)求中,有兩個(gè)請(qǐng)求耗時(shí)較為長(zhǎng)。

圖片

在Taro3的升級(jí)中,官方有提到預(yù)加載Preload,在小程序中,從調(diào)用 Taro.navigateTo 等路由跳轉(zhuǎn) API 后,到小程序頁(yè)面觸發(fā) onLoad 會(huì)有一定延時(shí)(約300ms,如果是分包新下載則跳轉(zhuǎn)時(shí)間更長(zhǎng)),因此一些網(wǎng)絡(luò)請(qǐng)求可以提前到發(fā)起跳轉(zhuǎn)時(shí)一起去請(qǐng)求。于是我們?cè)谠谔D(zhuǎn)前,使用Taro.preload預(yù)先加載復(fù)雜列表的請(qǐng)求:

// Page A
const query = new Query({
// ...
})


Taro.preload({
RequestPromise: requestPromiseA({data: query }),
})
// Page B
componentDidMount() {
// 在跳轉(zhuǎn)的過(guò)程中,發(fā)出請(qǐng)求,因?yàn)榉祷氐氖且粋€(gè)promise,所以需要在B頁(yè)面承接:
Taro.getCurrentInstance().preloadData?.RequestPromise?.then(res => {
this.setState(this.processResData(res.data))
})
}

用同樣的檢測(cè)方式反復(fù)測(cè)試后,使用preload的時(shí),能提前300~400ms提前拿到酒店的列表數(shù)據(jù)。

圖片

圖片

左邊是沒(méi)使用preload的舊列表,右邊是預(yù)加載的列表,能明顯看出預(yù)加載后的列表會(huì)快一些。

圖片

然而在實(shí)際的使用中我們發(fā)現(xiàn)preload存在部分缺陷,對(duì)于承接頁(yè)面,如果接口較為復(fù)雜,會(huì)對(duì)業(yè)務(wù)流程的代碼有一定的入侵。究其本質(zhì),是前置了網(wǎng)絡(luò)請(qǐng)求,所以我們可以對(duì)網(wǎng)絡(luò)請(qǐng)求部分加入緩存策略,即可達(dá)到該效果,且接入成本會(huì)大大降低。

3.2  合理運(yùn)用setData

setData 是小程序開發(fā)中使用最頻繁、也是最容易引發(fā)性能問(wèn)題的API。setData 的過(guò)程,大致可以分成幾個(gè)階段:

  • 邏輯層虛擬 DOM 樹的遍歷和更新,觸發(fā)組件生命周期和 observer 等;
  • 將 data 從邏輯層傳輸?shù)揭晥D層;
  • 視圖層虛擬 DOM 樹的更新、真實(shí) DOM 元素的更新并觸發(fā)頁(yè)面渲染更新。

數(shù)據(jù)傳輸?shù)暮臅r(shí)與數(shù)據(jù)量的大小正相關(guān),舊的列表頁(yè)第一次加載的時(shí)候,一共請(qǐng)求了4個(gè)接口,setData短時(shí)間里有6次,數(shù)據(jù)量偏大的有兩次,我們嘗試的優(yōu)化方式為,將數(shù)據(jù)量大的兩次分開,另外五次發(fā)現(xiàn)都是一些零散的狀態(tài)和數(shù)據(jù),可以作為一次。

圖片

指標(biāo)

setData次數(shù)

setData耗時(shí)(ms)

減少耗時(shí)百分比

第一次進(jìn)入列表頁(yè)

3

2182

9.23%

進(jìn)行完這一步的操作,平均能減少200ms左右,效果較小,因?yàn)轫?yè)面的節(jié)點(diǎn)數(shù)沒(méi)變,setData主要的耗時(shí)還分布于渲染時(shí)間。

3.3  優(yōu)化頁(yè)面的節(jié)點(diǎn)數(shù)

根據(jù)微信官方文檔的說(shuō)明,一個(gè)太大的節(jié)點(diǎn)樹會(huì)增加內(nèi)存使用的同時(shí),樣式重排時(shí)間上也會(huì)更長(zhǎng)。建議一個(gè)頁(yè)面節(jié)點(diǎn)數(shù)量應(yīng)少于 1000 個(gè),節(jié)點(diǎn)樹深度少于 30 層,子節(jié)點(diǎn)數(shù)不大于 60 個(gè)。 

在微信開發(fā)者工具中分析該頁(yè)面兩個(gè)模塊存在大量的節(jié)點(diǎn)數(shù)。一個(gè)是篩選項(xiàng)模塊,一個(gè)是長(zhǎng)列表的模塊。因?yàn)檫@部分功能較多,且結(jié)構(gòu)復(fù)雜,我們采用了選擇性渲染。如在用戶瀏覽列表式,篩選項(xiàng)不生成具體節(jié)點(diǎn)。點(diǎn)擊展開篩選的時(shí)候再渲染出節(jié)點(diǎn),對(duì)于頁(yè)面列表的體驗(yàn)有一定程度的緩解。另一方面,對(duì)于整體布局的書寫上,有意識(shí)的避免嵌套過(guò)深的寫法,如RichText使用,部分選擇圖片代替等。

3.4  優(yōu)化篩選項(xiàng)相關(guān)

3.4.1  改變動(dòng)畫方式

在重構(gòu)篩選項(xiàng)的過(guò)程中,發(fā)現(xiàn)在一些機(jī)型上,小程序的動(dòng)畫效果不太理想,比如當(dāng)打開篩選項(xiàng)tab的時(shí)候,需要實(shí)現(xiàn)一個(gè)向下拉出的效果,早期在實(shí)現(xiàn)的時(shí)候,會(huì)出現(xiàn)兩個(gè)問(wèn)題:

  • 動(dòng)畫會(huì)閃一下 然后再出現(xiàn)
  • 篩選頁(yè)面節(jié)點(diǎn)過(guò)多時(shí),點(diǎn)擊響應(yīng)過(guò)慢,用戶體驗(yàn)差

圖片

舊的篩選項(xiàng)的動(dòng)畫是通過(guò)keyframes方式實(shí)現(xiàn)了一個(gè)fadeIn的動(dòng)畫,加在最外層,但是無(wú)論如何在動(dòng)畫出現(xiàn)的那一幀,都會(huì)閃一下。分析下來(lái),因?yàn)閗eyframes執(zhí)行動(dòng)畫造成的卡頓:

.filter-wrap {
animation: .3s ease-in fadeIn;
}


@keyframes fadeIn {
0% {
transform: translateY(-100%)
}
100% {
transform: translateY(0)
}
}

于是,嘗試換了一種實(shí)現(xiàn)方式,通過(guò)transition來(lái)實(shí)現(xiàn)transfrom:

.filter-wrap {
transform: translateY(-100%);
transition: none;
&.active {
transform: translateY(0);
transition: transform .3s ease-in;
}
}

圖片

3.4.2  維護(hù)簡(jiǎn)潔的state

操作篩選項(xiàng)的時(shí)候,每操作一次都需要根據(jù)唯一id從篩選項(xiàng)的數(shù)據(jù)結(jié)構(gòu)中循環(huán)遍歷,去找到對(duì)應(yīng)的item,改掉item的狀態(tài),然后將整個(gè)結(jié)構(gòu)重新setState。官方文檔中提到關(guān)于setState,應(yīng)該盡量避免處理過(guò)大的數(shù)據(jù),會(huì)影響頁(yè)面的更新性能。 

針對(duì)這一問(wèn)題,采取的辦法是:

  • 預(yù)先將復(fù)雜的對(duì)象扁平化,示例如下:

{
"a": {
"subs": [{
"a1": {
"subs": [{
"id": 1
}]
}
}]
},
"b": {
"subs": [{
"id": 2
}]
},


// ...
}

扁平化后的篩選項(xiàng)數(shù)據(jù)結(jié)構(gòu):

{
"1": {
"id": 1,
"name": "漢庭",
"includes": [],
"excludes": [],
// ...
},
"2": {
// ...
},


// ...
}

  • 不改變?cè)械臄?shù)據(jù),利用扁平化后的數(shù)據(jù)結(jié)構(gòu)維護(hù)一個(gè)動(dòng)態(tài)的選中列表:

const flattenFilters = data => {
// ...


return {
[id]: {
id: 2,
name: "全季",
includes: [],
excludes: []
// ...
},


// ...
}
}


const filters = [], filtersSelected = {}
const flatFilters = flattenFilters(filters)


const onClickFilterItem = item => {


// 所有的操作需要先拿到扁平化的item
const flatItem = flatFilters[item.id]


if (filtersSelected[flatItem.id]) {
// 已選中,需要取消選中
delete filtersSelected[flatItem.id]
}
else {
// 未選中,需要選中
filtersSelected[flatItem.id] = flatItem
// 取消選中排斥項(xiàng)
const idsSelected = Object.keys(filtersSelected)
const idsIntersection = intersection(idsSelected, flatItem.selfExcludes) // 交集
if (idsIntersection.length) {
idsIntersection.forEach(id => {
delete filtersSelected[id]
})
}


// 其他邏輯 (快篩,關(guān)鍵詞等)
}


this.setState({filtersSelected})
}

上面是一個(gè)簡(jiǎn)單的實(shí)現(xiàn),前后對(duì)比,我們只需要維護(hù)一個(gè)很簡(jiǎn)單的對(duì)象,對(duì)其屬性進(jìn)行添加或者刪除,性能有細(xì)微的提高,且代碼更為簡(jiǎn)單整潔。在業(yè)務(wù)代碼中,類似這種通過(guò)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換提升效率的地方有很多。

關(guān)于篩選項(xiàng),可以對(duì)比下檢測(cè)的平均數(shù)據(jù),減少200ms~300ms,也會(huì)得到一些提升:

指標(biāo)

setData耗時(shí)舊

setData耗時(shí)新

減少耗時(shí)百分比

長(zhǎng)列表下篩選項(xiàng)展開

1023

967

5.47%

長(zhǎng)列表下點(diǎn)擊篩選項(xiàng)

1758

1443

17.92%

3.5  長(zhǎng)列表的優(yōu)化

早期酒店列表頁(yè)引入了虛擬列表,針對(duì)長(zhǎng)列表渲染一定數(shù)目的酒店。核心的思路是只渲染顯示在屏幕的數(shù)據(jù),基本實(shí)現(xiàn)就是監(jiān)聽(tīng) scroll 事件,并且重新計(jì)算需要渲染的數(shù)據(jù),不需要渲染的數(shù)據(jù)留一個(gè)空的 div 占位元素。

  • 加載下一頁(yè)有輕微的卡頓:

圖片

通過(guò)數(shù)據(jù)發(fā)現(xiàn),下拉更新列表平均耗時(shí)1900ms左右:

指標(biāo)

setData次數(shù)

setData耗時(shí)

下拉列表更新

3

1903

針對(duì)這個(gè)問(wèn)題,解決方案是,提前加載下一頁(yè)的數(shù)據(jù),將下一頁(yè)存入內(nèi)存變量中。滾動(dòng)加載的時(shí)候直接從內(nèi)存變量中去取,然后setData更新到數(shù)據(jù)中。

圖片

  • 滑動(dòng)速度過(guò)快會(huì)出現(xiàn)白屏(速度越快白屏?xí)r間越久,下方左圖):虛擬列表的原理就是利用空的View去占位,當(dāng)快速回滾的時(shí)候,渲染的時(shí)候當(dāng)節(jié)點(diǎn)過(guò)于復(fù)雜,特別是酒店帶有圖片,渲染就會(huì)變慢,導(dǎo)致白屏,我們進(jìn)行了三種方案的嘗試:1)  使用動(dòng)態(tài)的骨架圖代替原有的View占位 下方圖右:

圖片

圖片

2)  CustomWrapper

為了提升性能,官方推薦了CusomWrapper,它可以將包裹的組件與頁(yè)面隔離,組件渲染時(shí)不會(huì)更新整個(gè)頁(yè)面,由page.setData變?yōu)閏omponent.setData。

自定義組件是基于Shadow DOM實(shí)現(xiàn)的,對(duì)組件中的DOM和CSS進(jìn)行了封裝,使得組件內(nèi)部與主頁(yè)面的DOM保持了分離。圖片中的#shadow-root是根節(jié)點(diǎn),成為影子根,和主文檔分開渲染。#shadow-root可以嵌套形成節(jié)點(diǎn)樹(Shadow Tree)

<custom-wrapper is="custom-wrapper">
#shadow-root
<view class="list"></view>
</custom-wrapper>

包裹的組件被隔離,這樣內(nèi)部的數(shù)據(jù)的更新不會(huì)影響到整個(gè)頁(yè)面,可以簡(jiǎn)單看下低性能客戶端下的表現(xiàn)。效果還是明顯的,同一時(shí)間點(diǎn)擊,右側(cè)彈窗出現(xiàn)的耗時(shí)平均會(huì)快200ms ~ 300ms (同一機(jī)型同一環(huán)境下測(cè)出),機(jī)型越低端越明顯。

圖片

(右側(cè)是CustomWrapper下的)

3)  使用小程序原生組件

用小程序的原生組件去實(shí)現(xiàn)這個(gè)列表Item。原生組件繞過(guò)Taro3的運(yùn)行時(shí),也就是說(shuō),在用戶對(duì)頁(yè)面操作的時(shí)候,如果是taro3的組件,需要進(jìn)行前后數(shù)據(jù)的diff計(jì)算,然后生產(chǎn)新的虛擬dom所需要的節(jié)點(diǎn)數(shù)據(jù),進(jìn)而調(diào)用小程序的api去對(duì)節(jié)點(diǎn)進(jìn)行操作。原生組件繞過(guò)了這一些列的操作,直接是是底層小程序?qū)?shù)據(jù)的更新。所以,縮短了一些時(shí)間??梢钥匆幌聦?shí)現(xiàn)后的效果:

圖片

指標(biāo)

setData次數(shù)(舊)

setData次數(shù)(新)

下拉列表更新

3

1

setData耗時(shí)(舊)

  setData耗時(shí)(新)

  減少耗時(shí)百分比

1903

836

56.07%

可以看出原生性能提升很大,平均更新列表縮短1s左右,但是使用原生也有缺點(diǎn),主要表現(xiàn)為以下兩個(gè)方面:

  • 組件包含的所有樣式 需要按照小程序的規(guī)范寫一遍,且與taro的樣式相互隔離;
  • 在原生組件中無(wú)法使用taro的API,比如createSelectorQuery這種;

對(duì)比三種方案,性能提升逐步加強(qiáng)??紤]到使用Taro原本的意義在于跨端,如果使用原生,就沒(méi)辦法達(dá)到這個(gè)目的,不過(guò)我們?cè)趪L試是否可以通過(guò)插件,在編譯時(shí)生成對(duì)應(yīng)原生小程序的組件代碼,以此解決這一問(wèn)題,最終達(dá)到最優(yōu)效果。

3.6  React.memo

當(dāng)復(fù)雜頁(yè)面子組件過(guò)多時(shí),父組件的渲染會(huì)導(dǎo)致子組件跟著渲染,React.memo可以做淺層的比較防止不必要的渲染:

const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
})

React.memo為高階組件。它與React.PureComponent非常相似,但它適用于函數(shù)組件,但不適用于 class 組件。

如果你的函數(shù)組件在給定相同props的情況下渲染相同的結(jié)果,那么你可以通過(guò)將其包裝在React.memo中調(diào)用,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。

默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比,如果你想要控制對(duì)比過(guò)程,那么請(qǐng)將自定義的比較函數(shù)通過(guò)第二個(gè)參數(shù)傳入來(lái)實(shí)現(xiàn)。

function MyComponent(props) {
/* 使用 props 渲染 */
}


function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 傳入 render 方法的返回結(jié)果與
將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true,
否則返回 false
*/
}


export default React.memo(MyComponent, areEqual);

四、總結(jié)

本次復(fù)雜列表的性能優(yōu)化我們前后經(jīng)歷較久,嘗試了各種可能的優(yōu)化點(diǎn)。從列表頁(yè)的預(yù)加載,篩選項(xiàng)數(shù)據(jù)結(jié)構(gòu)和動(dòng)畫實(shí)現(xiàn)的改變,到長(zhǎng)列表的體驗(yàn)優(yōu)化和原生的結(jié)合,提升了頁(yè)面的更新和渲染效率,目前仍密切關(guān)注,繼續(xù)保持探索。

以下為最終效果對(duì)比(右側(cè)為優(yōu)化后):

圖片

圖片


責(zé)任編輯:未麗燕 來(lái)源: 攜程技術(shù)
相關(guān)推薦

2021-07-05 14:55:28

前端優(yōu)化圖片

2019-03-15 15:00:49

Webpack構(gòu)建速度前端

2015-09-16 15:21:23

Android性能優(yōu)化內(nèi)存

2021-08-02 10:50:57

性能微服務(wù)數(shù)據(jù)

2015-09-16 15:48:55

Android性能優(yōu)化電量

2015-09-16 14:37:50

Android性能優(yōu)化運(yùn)算

2015-09-16 13:54:30

Android性能優(yōu)化渲染

2024-01-03 08:20:05

Java字符串性能

2018-07-18 12:12:20

Spark大數(shù)據(jù)代碼

2009-08-10 14:48:39

ASP.NET組件設(shè)計(jì)

2009-08-13 15:49:18

ASP.NET性能優(yōu)化

2019-03-26 10:02:16

WebpackJavascript前端

2009-08-13 16:22:18

ASP.NET性能優(yōu)化

2020-06-11 13:03:04

性能優(yōu)化緩存

2011-05-27 16:00:10

DB2

2024-02-20 19:53:57

網(wǎng)絡(luò)通信協(xié)議

2010-04-13 11:26:54

IIS網(wǎng)站服務(wù)器性能

2016-12-21 12:46:47

數(shù)據(jù)倉(cāng)庫(kù)SQLHive

2012-06-20 13:54:44

架構(gòu)性能優(yōu)化

2021-06-02 08:25:44

性能優(yōu)化Repository
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)