先說需求
需求定義很簡單,本來有一個這樣的瀑布流頁面,滾動載入更多卡片,在此基礎上,增加視訊支援,也就是說可能是圖片也可能是短視訊。視訊要求在Wi-Fi時內聯靜音自動迴圈播放,不需要其他互動。
是不是非常簡單的一句需求,要求也不高。
調研了視訊內聯播放的相容性,問題不大。
靜音播放也就一個muted屬性的事情。
自動播放只需要mounted之後play一下,或者用autoplay屬性。
迴圈播放就是loop屬性啦。
判斷Wi-Fi環境可以通過橋與客戶端通訊,這個涉及到具體業務,就不細說了,總之,調個API完事。
看起來已經實現了。
開始踩坑。
遇到效能問題
按照需求,後端會控制視訊出現的頻率,至少5個卡片才允許出一個視訊,不然滿螢幕視訊,效果不好。但這是一個瀑布流,理論上可以滾動載入成百上千個卡片,假設每5個卡片放一個視訊,效果會如何呢?
大概也就是這樣吧,CPU溫度可以上90,佔用率300%+(四核八執行緒),用上那什麼iphone-inline-video外掛相容iOS 9的話,可以上600%+。
表現在移動端就是滾都滾不動,即使是iPhone X,滑動起來也是非常無比卡頓。
嘗試優化
現實世界中,使用者一個螢幕最多看到幾個卡片,基本不可能超過10個,但可能已經載入了上百張卡片,裡面的視訊都還在自動播放,CPU根本吃不消。
所以優化思路是,停止掉不需要的視訊。但我的做法靴微激進了點,參考點評APP首頁的效果,滾到下面再滾回去圖片都是重新載入的,至少看起來是的,也許圖片、視訊資源都被GC了,而不僅僅是暫停視訊。
我的實踐是,不在可視區域的圖片和視訊直接替換成空div,使用者滾回來的時候再替換回來。至於圖片會不會被GC,交給容器去做。
那麼,怎麼實現這個效果呢?
一開始我在鑽牛角尖的糾結,如何在父元件list中監聽滾動,判斷螢幕顯示了哪些子元件card,並通知某些子元件你被優化了。
好像不是很好做,那要不在每個子元件裡自己監聽一下滾動,判斷自己的位置在不在可視區域?感覺效能會很差,畢竟滾動事件本身就會觸發很多很多次,還要搞這麼多監聽器。
後來,我參考了lazysizes的實現,它是通過在全域性window掛了一個lazyElements列表,在初始化的時候直接querySelectorAll('.custom-class-name')…… 大致思路就是:首先把所有元素加了個特殊的類名,初始化時取出來存在window物件下,然後監聽滾動,forEach直接遍歷所有元素,根據該元素的位置(是否在可視區域內)來判斷是否需要載入。
考慮到我們頻繁使用的lazysizes也不過就這樣處理的,那我也可以這麼做吧。主要實現的程式碼如下:
export default {
mounted() {
this.addOptimizeScrollListener();
},
beforeDestroy() {
document.removeEventListener('scroll', this._optimizeListener, false);
},
methods: {
addOptimizeScrollListener() {
const windowHeight = window.innerHeight;
const TOLERANCE_HEIGHT = 300;
this._optimizeListener = throttleByRAF(() => {
if (this.$refs.cards) {
this.$refs.cards.forEach(card => {
const rect = card.$el.getBoundingClientRect();
if (rect.top > windowHeight + TOLERANCE_HEIGHT || rect.bottom < 0 - TOLERANCE_HEIGHT) {
card.isOptimized = true;
} else {
card.isOptimized = false;
}
});
}
});
document.addEventListener('scroll', this._optimizeListener, false);
},
}
};
複製程式碼
簡單解釋一下:在mounted的時候新增滾動監聽器。監聽器做的事情就是通過$refs
拿到cards元件列表,然後類似lazysizes的操作,直接forEach判斷是否在視窗內,然後給該card元件例項的isOptimized
賦值。這裡有一些優化,比如throttleByRAF用來限流執行,TOLERANCE_HEIGHT用來容錯,不要這麼嚴格的按照視窗邊界來優化,減少使用者輕微滾動導致的重複載入。
主要的實現就是這裡了,card元件內部只需要根據isOptimized
的值來決定渲染圖片視訊還是空白就可以了。
值得注意的是,必須要獲取到原有的圖片視訊的高度,否則就會有抖動,甚至瀑布流佈局錯亂。這裡比較特殊的一點是,後端的介面裡提供了寬高,可以提前預知高度,所以只需要按照給的高度填充空白即可。
其實也可以把整個card替換成空白佔位(當然我這裡說的空白不是純白色,是五顏六色的佔位,如果有想法,還可以設計一些佔點陣圖,提高視覺效果)。那怎麼拿到高度呢?
答案就是window.getComputedStyle
! 在mounted的時候獲取一下存起來,被優化的時候用這個高度即可。
效能對比
首先,來個直觀的體驗對比,那就是CPU佔用沒這麼誇張了,移動端滾起來也很快樂了,用起來和之前僅有圖片時沒有太多差別。
在Performance皮膚能觀察到Nodes數量大幅減少,未優化時大約16000+,優化後3000~6000個。
由於之前被誤導可能是記憶體佔用過大導致的,所以我詳細的對比了一下不同策略的記憶體使用情況。
果然……沒什麼區別。。。可以看到優化掉整個卡片還是能省一點記憶體的。粗略看了一下細節,未優化的情況下,其中VueComponents的數量為632,佔用8.9MB,優化後數量僅343個,佔用5.1MB。兩者的數量差值為289,可以說明當前只渲染11個卡片,符合預期。
然而這點差別相對來說還OK,因為我直接用了線上的圖片300張,每張只有20KB左右。主要還是解決了CPU佔用過高的問題,而回收DOM帶來的記憶體優化算是贈送的吧。
其他問題
判斷Wi-Fi導致卡頓
在較為古老的OPPO手機上測試該頁面,發現還是有些卡頓,嘗試把getNetworkType呼叫幹掉,就繼續絲滑了。
原因是每載入一個有視訊的卡片時,我都會去判斷一下網路型別,以應對使用者切換Wi-Fi到4G之類的操作。因為APP的橋沒有提供相應的監聽函式,只好這樣操作。但顯然,不是很值得。
於是找到了一個線上和離線事件,可以監聽瀏覽器上線和下線。如果使用者從Wi-Fi切換到4G時會經歷下線和上線,那真是完美了。當然,沒有這麼完美,切換網路過程中並沒有觸發這兩個事件……
最終的解決方案是,每10秒呼叫一次getNetworkType,用輪詢折衷一下。
靜音播放Bug
靜音播放視訊似乎沒有問題,但是當我點進一個詳情頁,開啟新的webview,再返回到當前頁面時,詭異的播出了聲音…… 並且,還沒有找到原因。
結果不做了
然後發現點評APP首頁用的是WebP格式的動圖,而不是視訊。。。
改方案。。。
改介面。。。
優化基本是白寫了,或者說,可以優化,但沒必要。。。
(還是學到了點東西的)
最後的結局
平臺對WebP支援的稀爛,動圖直接沒處理,暫時無法支援動圖……
所以還是要使用視訊。
好訊息是這個優化沒白做,壞訊息是我必須要解決靜音變成非靜音的bug。
直接提了工單給平臺,得知是對方手滑實現的“特性”,目前可以通過監聽webview appear事件手動再設定一下muted即可。
這…算是happy ending吧。