先說需求
需求定義很簡單,本來有一個這樣的瀑布流頁面,滾動加載更多卡片,在此基礎(chǔ)上,增加視頻支持,也就是說可能是圖片也可能是短視頻。視頻要求在Wi-Fi時內(nèi)聯(lián)靜音自動循環(huán)播放,不需要其他交互。
是不是非常簡單的一句需求,要求也不高。

調(diào)研了視頻內(nèi)聯(lián)播放的兼容性,問題不大。
靜音播放也就一個muted屬性的事情。
自動播放只需要mounted之后play一下,或者用autoplay屬性。
循環(huán)播放就是loop屬性啦。
判斷Wi-Fi環(huán)境可以通過橋與客戶端通信,這個涉及到具體業(yè)務(wù),就不細說了,總之,調(diào)個API完事。
看起來已經(jīng)實現(xiàn)了。
開始踩坑。
遇到性能問題
按照需求,后端會控制視頻出現(xiàn)的頻率,至少5個卡片才允許出一個視頻,不然滿屏幕視頻,效果不好。但這是一個瀑布流,理論上可以滾動加載成百上千個卡片,假設(shè)每5個卡片放一個視頻,效果會如何呢?

大概也就是這樣吧,CPU溫度可以上90,占用率300%+(四核八線程),用上那什么iphone-inline-video插件兼容iOS 9的話,可以上600%+。
表現(xiàn)在移動端就是滾都滾不動,即使是iPhone X,滑動起來也是非常無比卡頓。
嘗試優(yōu)化
現(xiàn)實世界中,用戶一個屏幕最多看到幾個卡片,基本不可能超過10個,但可能已經(jīng)加載了上百張卡片,里面的視頻都還在自動播放,CPU根本吃不消。
所以優(yōu)化思路是,停止掉不需要的視頻。但我的做法靴微激進了點,參考點評APP首頁的效果,滾到下面再滾回去圖片都是重新加載的,至少看起來是的,也許圖片、視頻資源都被GC了,而不僅僅是暫停視頻。
我的實踐是,不在可視區(qū)域的圖片和視頻直接替換成空div,用戶滾回來的時候再替換回來。至于圖片會不會被GC,交給容器去做。
那么,怎么實現(xiàn)這個效果呢?
一開始我在鉆牛角尖的糾結(jié),如何在父組件list中監(jiān)聽滾動,判斷屏幕顯示了哪些子組件card,并通知某些子組件你被優(yōu)化了。
好像不是很好做,那要不在每個子組件里自己監(jiān)聽一下滾動,判斷自己的位置在不在可視區(qū)域?感覺性能會很差,畢竟?jié)L動事件本身就會觸發(fā)很多很多次,還要搞這么多監(jiān)聽器。
后來,我參考了lazysizes的實現(xiàn),它是通過在全局window掛了一個lazyElements列表,在初始化的時候直接querySelectorAll('.custom-class-name')…… 大致思路就是:首先把所有元素加了個特殊的類名,初始化時取出來存在window對象下,然后監(jiān)聽滾動,forEach直接遍歷所有元素,根據(jù)該元素的位置(是否在可視區(qū)域內(nèi))來判斷是否需要加載。
考慮到我們頻繁使用的lazysizes也不過就這樣處理的,那我也可以這么做吧。主要實現(xiàn)的代碼如下:
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的時候添加滾動監(jiān)聽器。監(jiān)聽器做的事情就是通過$refs拿到cards組件列表,然后類似lazysizes的操作,直接forEach判斷是否在視窗內(nèi),然后給該card組件實例的isOptimized賦值。這里有一些優(yōu)化,比如throttleByRAF用來限流執(zhí)行,TOLERANCE_HEIGHT用來容錯,不要這么嚴格的按照視窗邊界來優(yōu)化,減少用戶輕微滾動導(dǎo)致的重復(fù)加載。
主要的實現(xiàn)就是這里了,card組件內(nèi)部只需要根據(jù)isOptimized的值來決定渲染圖片視頻還是空白就可以了。
值得注意的是,必須要獲取到原有的圖片視頻的高度,否則就會有抖動,甚至瀑布流布局錯亂。這里比較特殊的一點是,后端的接口里提供了寬高,可以提前預(yù)知高度,所以只需要按照給的高度填充空白即可。

其實也可以把整個card替換成空白占位(當然我這里說的空白不是純白色,是五顏六色的占位,如果有想法,還可以設(shè)計一些占位圖,提高視覺效果)。那怎么拿到高度呢?
答案就是window.getComputedStyle! 在mounted的時候獲取一下存起來,被優(yōu)化的時候用這個高度即可。
性能對比
首先,來個直觀的體驗對比,那就是CPU占用沒這么夸張了,移動端滾起來也很快樂了,用起來和之前僅有圖片時沒有太多差別。
在Performance面板能觀察到Nodes數(shù)量大幅減少,未優(yōu)化時大約16000+,優(yōu)化后3000~6000個。
由于之前被誤導(dǎo)可能是內(nèi)存占用過大導(dǎo)致的,所以我詳細的對比了一下不同策略的內(nèi)存使用情況。

果然……沒什么區(qū)別。。??梢钥吹絻?yōu)化掉整個卡片還是能省一點內(nèi)存的。粗略看了一下細節(jié),未優(yōu)化的情況下,其中VueComponents的數(shù)量為632,占用8.9MB,優(yōu)化后數(shù)量僅343個,占用5.1MB。兩者的數(shù)量差值為289,可以說明當前只渲染11個卡片,符合預(yù)期。
然而這點差別相對來說還OK,因為我直接用了線上的圖片300張,每張只有20KB左右。主要還是解決了CPU占用過高的問題,而回收DOM帶來的內(nèi)存優(yōu)化算是贈送的吧。
其他問題
判斷Wi-Fi導(dǎo)致卡頓
在較為古老的OPPO手機上測試該頁面,發(fā)現(xiàn)還是有些卡頓,嘗試把getNetworkType調(diào)用干掉,就繼續(xù)絲滑了。
原因是每加載一個有視頻的卡片時,我都會去判斷一下網(wǎng)絡(luò)類型,以應(yīng)對用戶切換Wi-Fi到4G之類的操作。因為APP的橋沒有提供相應(yīng)的監(jiān)聽函數(shù),只好這樣操作。但顯然,不是很值得。
于是找到了一個在線和離線事件,可以監(jiān)聽瀏覽器上線和下線。如果用戶從Wi-Fi切換到4G時會經(jīng)歷下線和上線,那真是完美了。當然,沒有這么完美,切換網(wǎng)絡(luò)過程中并沒有觸發(fā)這兩個事件……
最終的解決方案是,每10秒調(diào)用一次getNetworkType,用輪詢折衷一下。
靜音播放Bug
靜音播放視頻似乎沒有問題,但是當我點進一個詳情頁,打開新的webview,再返回到當前頁面時,詭異的播出了聲音…… 并且,還沒有找到原因。
結(jié)果不做了
然后發(fā)現(xiàn)點評APP首頁用的是WebP格式的動圖,而不是視頻。。。
改方案。。。
改接口。。。
優(yōu)化基本是白寫了,或者說,可以優(yōu)化,但沒必要。。。
(還是學(xué)到了點東西的)
最后的結(jié)局
平臺對WebP支持的稀爛,動圖直接沒處理,暫時無法支持動圖……
所以還是要使用視頻。
好消息是這個優(yōu)化沒白做,壞消息是我必須要解決靜音變成非靜音的bug。
直接提了工單給平臺,得知是對方手滑實現(xiàn)的“特性”,目前可以通過監(jiān)聽webview appear事件手動再設(shè)置一下muted即可。
這…算是happy ending吧。