小程序 多圖列表 性能優(yōu)化

打個小廣告:
如果你想獲取更多前端干貨,
歡迎關(guān)注我的個人微信公眾號:
前端夜談
還有一只可愛的橘貓等著你來吸~~

小程序 多圖列表 性能優(yōu)化

寫這篇文章的緣由: 最近在公司的小程序項目中遇到了頁面圖片元素過多導(dǎo)致的性能問題. 從小程序提供的性能檢測面板分析, 確定是圖片元素占用了過多內(nèi)存導(dǎo)致.

因為本人之前主要是做桌面端應(yīng)用開發(fā)和原生app開發(fā), 沒有太顧及過移動端圖片的內(nèi)存占用問題. 這次既然遇到了, 也就趁這個機會學(xué)習(xí)一下其優(yōu)化的技巧.

什么造成的性能問題

簡單的來說: DOM節(jié)點過多 && 圖片節(jié)點過多

DOM節(jié)點過多會造成更多的內(nèi)存占用. 按照目前的微信小程序限制, 內(nèi)存占用500M以上會出現(xiàn)卡頓, 甚至閃退. 如果列表中節(jié)點沒有圖片標(biāo)簽, 內(nèi)存占用現(xiàn)象還不會太明顯, 只是DOM節(jié)點過多會造成頁面渲染耗時增加. 但當(dāng)列表節(jié)點中含有圖片時, 內(nèi)存占用會迅速攀升.

如何解決這兩點呢?

對于上面兩點, 我們分別有對應(yīng)的優(yōu)化思路

1. DOM節(jié)點過多.

對于無限加載的頁面, 列表中每一個元素都有大量的子節(jié)點. 當(dāng)列表數(shù)目增加時, 頁面的總節(jié)點數(shù)會暴增. 以小紅書的小程序為例:

image

上圖中可以看到, 該頁面為很多的卡片組成的列表頁面. 假設(shè)一個卡片的DOM子節(jié)點數(shù)為 30, 那么當(dāng)列表元素加載到1000時, 頁面會有 30 * 1000 = 30,000 個DOM節(jié)點. 小程序顯然是吃不消的 (注: 微信小程序推薦總節(jié)點數(shù)不超過: 1000)

那我們該如何處理來減少節(jié)點數(shù)呢?

思路很簡單: 我們可以只對用戶當(dāng)前屏幕和上下兩屏進行真實內(nèi)容的加載, 對于其他用戶暫時不可見的地方, 用空白的節(jié)點進行占位. 這樣處理后, 實際有內(nèi)容的卡片數(shù)目不超過5個, 頁面的總節(jié)點數(shù)為: 5 * 30 + 995 = 1145. 相對于之前的節(jié)點數(shù)有了巨大的改進.

讓我們來看看代碼的實現(xiàn)

寫代碼前, 讓我們整理一下需要的數(shù)據(jù)結(jié)構(gòu).

首先這是一個列表頁面, 我們需要一個 List來保存頁面顯示的數(shù)據(jù): showCards. showCards 中只會保存5條真實數(shù)據(jù), 其余數(shù)據(jù)展示以空對象填充.

我們還需要一個保存所有真實數(shù)據(jù)的List, 這樣當(dāng)用戶滑動頁面時, 我們才能實時獲取需要顯示的卡片真實數(shù)據(jù): totalCards

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="javascript" cid="n65" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> Page({
showCards: [],
totalCards: []
})</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="" cid="n62" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> <view wx:for="{{showCards}}"
wx:key="{{index}}">
?
<self-define-component data-card-data="{{item}}">
</self-define-component>

</view></pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="javascript" cid="n71" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> async onReachBottom() {
const newCards = await fetchNewCards();
this.data.showCards.push(newCards);
this.setData({
showCards: this.data.showCards
})
},</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="" cid="n85" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> async onScrollCallback() {
try {
const rectList = await this.calcCardsHeight();
this.recycleCard(rectList);
} catch (e) {
console.error(e);
}
}
calcFeedHeight() {
return new Promise((resolve, reject) => {
this.createSelectorQuery()
.selectAll(.card)
.boundingClientRect(rectList => {
resolve(rectList);
})
.exec()
})
},
?
recycleCard(rectList) {
const newShowCards = [];
for (let i = 0; i < this.data.showCards.length; i++) {
const rect = rectList[i];
if (rect && Math.abs(rectList[i].top - 0) > pageHeight * 2) {
newShowCards.push({
type: 'empty-card',
height: rectList[i].bottom - rectList[i].top
});
} else {
const feed = totalCards[i];
newShowCards.push(feed);
}
}
this.setData({
showCards: newShowCards
});
}</pre>

<pre mdtype="fences" cid="n103" lang="" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" spellcheck="false" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> <view wx:for="{{showCards}}"
wx:key="{{index}}">
?
<view wx:if="{{item.type === 'empty-card'}}"
class="card empty-card"
style="height: {{item.height}}px">
</view>
?
<self-define-component wx:if="{{item.type !== 'empty-card'}}"
data-card-data="{{item}}"
class="card read-card">
</self-define-component>

</view></pre>

2. 圖片節(jié)點過多

通過上面一步的優(yōu)化, 我們其實已經(jīng)大幅減少了頁面加載的圖片數(shù)目. 但是有些情況, 我們的列表中的每一個卡片并不是只有一張圖, 有時我們的圖片組件是一個 swiper. 我們假設(shè)每個swiper平均展示10張圖片, 那么我們展示5張card的話,<Image/> 節(jié)點就有 50 個. 對于一些低端的安卓機, 這樣的開銷依然會造成卡頓.

那有什么好的優(yōu)化方案呢? 前面一個問題, 我們的優(yōu)化思路是在用戶看不見的地方, 將節(jié)點簡化展示.

同樣的, 對于swiper控件, 用戶能看到的也就是當(dāng)前圖片 以及 滑動可見的左右兩張圖片. 其余位置的圖片是可以簡化展示的. 從下圖可以看到, 其實需要立即加載的圖片只有三張. (紅色的框代表的是swiper組件的可視區(qū)域)

image

我們使用一個變量記錄當(dāng)前swiper控件展示圖片的坐標(biāo): curIndex, 然后我們改造一下 wxml布局文件. 代碼邏輯很簡單, 就是通過判斷當(dāng)前Image 節(jié)點的index和swiper展示節(jié)點的 index之間距離, 大于2就不顯示.

經(jīng)過這樣的處理后, 我們的每個swiper組件, 最多只會有三個占用實際內(nèi)存的 <Image/> 節(jié)點.

<pre mdtype="fences" cid="n118" lang="" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" spellcheck="false" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> <swiper-item wx:for="{{images}}"
wx:key="{{index}}">
?
<view >
<image class="img"
mode="widthFix"
src="{{index - curIndex < 2 && index - curIndex > -2 ? item.url : ''}}">
</image>
</view>
</swiper-item></pre>

https://github.com/ssthouse/ssthouse-blog

如果你對我的文章感興趣, 這里有我的一些 數(shù)據(jù)可視化瓷产, D3.js 方面的文章气筋, 歡迎 fork && star:

以上就是我在這次性能優(yōu)化中用到的一些小技巧, 希望能為你帶來一些幫助 :)

最后

這樣, 我們就解決了 DOM節(jié)點數(shù)目過多的問題. 并且最大限度的不影響用戶的體驗. (雖然用戶快速上下滑動時還是會看到一些空白, 但大多數(shù)情況用戶不會非常快速的上下滑, 而是閱讀內(nèi)容并慢速滑動)

接下來, 我們要對wxml布局文件進行相應(yīng)的修改:

接下來, 我們通過位置信息, 判斷是否展示card的真實數(shù)據(jù). 對于不展示真實數(shù)據(jù)的card, 我們需要保存其高度信息, 以便在渲染頁面時使用高度信息填充頁面. 同時我們給空card一個 type 屬性, 方便我們在 wxml中渲染時判斷卡片類型.

回調(diào)中, 我們首先通過小程序提供的獲取頁面元素位置的api: createSelectorQuery().boundingClientRect 來拿到每個卡片的位置信息.

在 Page 的 onPageScroll 回調(diào)中, 我們進行回收函數(shù)的調(diào)用 (注意這里回調(diào)時要進行節(jié)流處理, 否則頻繁調(diào)用會導(dǎo)致頁面閃動) . 讓我們看看這個回收頁面節(jié)點函數(shù)的主要邏輯:

接下來我們實現(xiàn)優(yōu)化DOM節(jié)點的代碼邏輯. 我們會再用戶滑動頁面(onScroll事件) 時, 對當(dāng)前頁面每個card 的位置進行判斷, 如果該 card在用戶可見范圍內(nèi)的上下兩屏內(nèi), 則展示真實數(shù)據(jù), 否則將其替換為寬高與原卡片一致的空白占位節(jié)點.

我們先實現(xiàn)沒有優(yōu)化DOM節(jié)點的代碼邏輯. 在頁面滑動到最底部時, 向showCards push進新的卡片, 并通過 setData 更新頁面. 這樣就實現(xiàn)了一個簡單的下拉無限加載的列表頁面.

簡單的代碼框架就是這樣 (這里省略了很多不影響理解思路的代碼細節(jié))

接下來我們來寫頁面布局部分:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌踏施,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罕邀,死亡現(xiàn)場離奇詭異畅形,居然都是意外死亡,警方通過查閱死者的電腦和手機诉探,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門日熬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阵具,你說我怎么就攤上這事碍遍《ㄍ” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵怕敬,是天一觀的道長揣炕。 經(jīng)常有香客問我,道長东跪,這世上最難降的妖魔是什么畸陡? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮虽填,結(jié)果婚禮上丁恭,老公的妹妹穿的比我還像新娘。我一直安慰自己斋日,他們只是感情好牲览,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恶守,像睡著了一般第献。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兔港,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天庸毫,我揣著相機與錄音,去河邊找鬼衫樊。 笑死飒赃,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的科侈。 我是一名探鬼主播载佳,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兑徘!你這毒婦竟也來了刚盈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挂脑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后欲侮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崭闲,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年威蕉,在試婚紗的時候發(fā)現(xiàn)自己被綠了刁俭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡韧涨,死狀恐怖牍戚,靈堂內(nèi)的尸體忽然破棺而出侮繁,到底是詐尸還是另有隱情,我是刑警寧澤如孝,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布宪哩,位于F島的核電站,受9級特大地震影響第晰,放射性物質(zhì)發(fā)生泄漏锁孟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一茁瘦、第九天 我趴在偏房一處隱蔽的房頂上張望品抽。 院中可真熱鬧,春花似錦甜熔、人聲如沸圆恤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盆昙。三九已至,卻和暖如春烧颖,著一層夾襖步出監(jiān)牢的瞬間弱左,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工炕淮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拆火,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓涂圆,卻偏偏與公主長得像们镜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子润歉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容