無限捲軸的複雜度

簡言之,請重複使用 DOM 元素,並移除距離可視區域較遠的元素。使用預留位置來處理延遲的資料。以下是無限捲動器的示範程式碼

網路上隨處可見無限捲動功能。Google 音樂的藝人清單、Facebook 的時間軸和 Twitter 的即時動態消息也是如此。你向下捲動,在抵達底部前,新內容會突然出現,使用者體驗流暢,而且很容易看出吸引力。

不過,無限捲動器背後的技術挑戰比想像中更困難。當您想做對的事™ 時,會遇到各種各樣的問題。首先是簡單的項目,例如內容不斷將頁尾往外推,導致頁尾中的連結幾乎無法觸及。但問題會越來越難。當使用者將手機從直向轉為橫向時,您會如何處理大小調整事件?或者,當清單過長時,您會如何防止手機停止運作?

The right thing™

我們認為這足以成為開發參考實作的理由,以可重複使用的方式解決所有這些問題,同時維持效能標準。

我們將使用 3 種技術達成目標:DOM 資源回收、墓碑和捲動錨點。

我們的示範案例是類似 Hangouts 的即時通訊視窗,您可以在其中捲動瀏覽訊息。首先,我們需要無窮無盡的即時通訊訊息來源。從技術上來說,目前沒有任何無限捲動器是真正無限的,但由於可匯入這些捲動器的資料量龐大,因此幾乎可說是無限。為求簡單,我們只會硬式編碼一組即時通訊訊息,並隨機挑選訊息、作者和偶爾的圖片附件,並加入少許人為延遲,讓行為更像真實網路。

Chat 應用程式螢幕截圖

DOM 資源回收

DOM 重複使用是未充分利用的技術,可讓 DOM 節點數量維持在低點。一般概念是使用螢幕外的現有 DOM 元素,而非建立新元素。誠然,DOM 節點本身很便宜,但並非免費,因為每個節點都會增加記憶體、版面配置、樣式和繪製方面的額外成本。如果網站的 DOM 過大,低階裝置會明顯變慢,甚至無法使用。此外,請注意,每當從節點新增或移除類別時,系統都會觸發重新配置和重新套用樣式的程序,而 DOM 越大,這類程序的成本就越高。回收 DOM 節點表示我們會大幅減少 DOM 節點總數,加快所有這些程序。

第一個障礙是捲動本身。由於我們在 DOM 中任何時間都只會有可用項目的一小部分,因此我們需要找到其他方法,讓瀏覽器的捲軸正確反映理論上存在的內容量。我們會使用 1 像素 x 1 像素的哨兵元素,並搭配轉換效果,強制讓含有項目的元素 (即跑道) 達到所需高度。我們會將跑道中的每個元素提升至各自的圖層,確保跑道本身的圖層完全空白。沒有背景顏色,如果跑道的圖層不為空白,則不符合瀏覽器的最佳化條件,我們必須在顯示卡上儲存高度為數十萬像素的紋理。絕對不適合在行動裝置上使用。

每當我們捲動時,都會檢查檢視區塊是否已充分接近跑道的結尾。如果是,我們會移動前哨元素,並將已離開檢視區塊的項目移至跑道底部,然後填入新內容,藉此延長跑道。

Runway Sentinel Viewport

反向捲動也是如此。不過,我們絕不會縮短實作中的緩衝區,確保捲軸位置保持一致。

墓碑

如先前所述,我們盡量讓資料來源的行為與現實世界中的事物類似。包括網路延遲等。這表示如果使用者使用輕拂捲動,他們可以輕鬆捲動經過我們有資料的最後一個元素。如果發生這種情況,我們會放置墓碑項目 (也就是預留位置),待資料送達後,就會以實際內容的項目取代。墓碑也會回收,並為可重複使用的 DOM 元素提供獨立集區。我們需要這項資訊,才能從墓碑順利轉換至填入內容的項目,否則使用者會感到非常突兀,甚至可能因此失去焦點。

Such
tomb. 非常堅硬。超酷的!

有趣的是,由於每個項目文字量不同或附有圖片,實際項目可能比墓碑項目高。為解決這個問題,我們會在每次資料傳入且視埠上方的墓碑遭到取代時,調整目前的捲動位置,並將捲動位置錨定至元素,而非像素值。這項概念稱為捲動錨定。

捲動錨定

當墓碑遭到取代,以及視窗大小變更時 (裝置翻轉時也會發生這種情況!),系統都會叫用捲動錨定功能。我們必須找出可視區域中最上方的可見元素。由於該元素可能只會部分顯示,我們也會儲存可視區域開頭與元素頂端的偏移量。

捲動錨定圖表。

如果視埠大小調整,且跑道有所變更,我們就能還原與使用者視覺上相同的狀況。獲勝!但如果視窗大小調整過,每個項目可能都會改變高度,因此我們如何得知錨定內容應放置在多遠的位置?我們不會!如要找出這個值,我們必須為錨定項目上方的每個元素配置版面,並加總所有元素的高度;這可能會在調整大小後造成明顯的暫停,而我們不希望發生這種情況。我們改為假設上方每個項目的大小都與墓碑相同,並據此調整捲動位置。當元素捲動到跑道時,我們會調整捲動位置,有效延遲版面配置工作,直到實際需要時才執行。

版面配置

我略過了一個重要細節:版面配置。如果每次回收 DOM 元素時都重新配置整個跑道,影格率就會遠低於每秒 60 個影格的目標。為避免這種情況,我們將版面配置的負擔轉移到自己身上,並使用具有轉換的絕對位置元素。這樣一來,我們就能假裝跑道上方的所有元素仍佔用空間,但實際上只有空白空間。由於我們自行執行版面配置,因此可以快取每個項目的結束位置,並在使用者向後捲動時,立即從快取載入正確的元素。

理想情況下,項目附加至 DOM 時只會重新繪製一次,且不會受到跑道中其他項目新增或移除的影響。這項功能僅適用於新式瀏覽器。

尖端技術調整

最近 Chrome 新增了對 CSS Containment 的支援,這項功能可讓開發人員告知瀏覽器,某個元素是版面配置和繪製工作的界線。由於我們在這裡自行進行版面配置,因此這是非常適合使用容器的應用程式。每當我們將元素新增至跑道時,知道其他項目不需要受到重新配置的影響。因此每個項目都應取得 contain: layout。我們也不想影響網站的其他部分,因此跑道本身也應取得這個樣式指令。

我們也考慮使用 IntersectionObservers 機制,偵測使用者捲動的距離是否夠遠,以便開始回收元素並載入新資料。不過,IntersectionObserver 指定為高延遲 (如同使用 requestIdleCallback),因此與不使用 IntersectionObserver 相比,我們可能會感覺回應速度較慢。即使是目前使用 scroll 事件的實作項目,也會受到這個問題影響,因為系統會盡量分派捲動事件。最終,Houdini 的 Compositor Worklet 會是這個問題的高保真解決方案。

但仍不盡完美

我們目前實作的 DOM 資源回收機制並不理想,因為它會加入視埠中的所有元素,而不是只處理實際顯示在畫面上的元素。也就是說,當您非常快速捲動時,Chrome 會因為要處理大量版面配置和繪製工作而跟不上速度。你只會看到背景。這不是世界末日,但絕對有改善空間。

希望您能瞭解,如果想兼顧優質使用者體驗和高標準效能,簡單的問題也可能變得相當棘手。隨著漸進式網頁應用程式成為行動裝置的核心體驗,這項指標將變得更加重要,網頁開發人員也必須持續投入資源,使用符合效能限制的模式。

所有程式碼都位於我們的存放區。我們已盡力確保其可重複使用,但不會在 npm 上發布為實際程式庫,也不會發布為個別存放區。主要用途為教育。