圖片視訊瀑布流長列表效能優化實踐

不愛薯條的土豆控發表於2019-01-29

先說需求

需求定義很簡單,本來有一個這樣的瀑布流頁面,滾動載入更多卡片,在此基礎上,增加視訊支援,也就是說可能是圖片也可能是短視訊。視訊要求在Wi-Fi時內聯靜音自動迴圈播放,不需要其他互動。

是不是非常簡單的一句需求,要求也不高。

瀑布流

調研了視訊內聯播放的相容性,問題不大。

靜音播放也就一個muted屬性的事情。

自動播放只需要mounted之後play一下,或者用autoplay屬性。

迴圈播放就是loop屬性啦。

判斷Wi-Fi環境可以通過橋與客戶端通訊,這個涉及到具體業務,就不細說了,總之,調個API完事。

看起來已經實現了。

開始踩坑。

遇到效能問題

按照需求,後端會控制視訊出現的頻率,至少5個卡片才允許出一個視訊,不然滿螢幕視訊,效果不好。但這是一個瀑布流,理論上可以滾動載入成百上千個卡片,假設每5個卡片放一個視訊,效果會如何呢?

CPU佔用情況

大概也就是這樣吧,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吧。

相關文章