無限捲軸的複雜度

羅伯特弗拉克
Robert Flack

重點摘要:重複使用 DOM 元素,並移除距離可視區域較遠的元素。使用預留位置考量延遲資料。請參考以下示範和無限捲動器的程式碼

無限捲動器會在網際網路上彈出。Google Music 的藝人清單會顯示 Facebook 的時間軸 也會顯示為 Twitter 的即時動態只要向下捲動,在到達底部之前,新內容看起來似乎就在不尋常。這是讓使用者享有流暢的使用體驗,也能輕鬆找出吸引力。

然而,無限捲動工具背後的技術挑戰要比預期更難。您希望能對 The Right ThingTM 尋求支援的問題範圍相當龐大。一開始,例如頁尾中的連結無法進入實際連結,因為內容不斷推掉頁尾。但問題會越來越難。當使用者將手機從直向轉為橫向時,或是如何避免手機在清單太長時閃爍至痛苦停擺,應該如何處理?

正確的東西 TM

因此,我們想想出一個參考實作項目,說明如何以可重複使用的方式處理所有這些問題,同時維持效能標準。

我們將使用 3 種技術來達成目標:DOM 回收、空值標記和捲動錨定。

我們以類似 Hangouts 的即時通訊視窗為例,讓我們可以捲動查看訊息。首先,我們需要的 聊天訊息是無窮無邊的嚴格說來,沒有無限的捲動器「完全」沒有無限捲動器,但可供提供給這些捲動器的資料量也相當龐大。為求簡單起見,我們僅使用硬式編碼的一組即時通訊訊息進行硬式編碼,並隨機挑選訊息、作者及偶爾出現的圖片附件,加上人工延遲的少量延遲,讓行為看起來與實際網路略有不同。

即時通訊應用程式螢幕截圖

DOM 回收

DOM 回收使用率偏低的技術,有助於降低 DOM 節點數量。一般來說,使用已建立的螢幕外的 DOM 元素,而非建立新的元素。允許 DOM 節點本身便宜,但不是免費的,因為每個節點都會增加記憶體、版面配置、樣式和繪製費用。如果網站有過大需要管理的 DOM,則無法完全無法使用時,低階裝置的運作速度會明顯變慢。另外也請留意,每次重組及重新套用樣式 (在節點中新增或移除類別時都會觸發的程序),使用大型 DOM 時成本會更高。回收 DOM 節點表示 DOM 節點總數會大幅減少,以加快所有程序。

第一個環節是捲動本身。由於不論在任何特定時間,我們在 DOM 中只有一小部分可用項目,因此我們需要尋找另一種方法,讓瀏覽器的捲軸能正確反映理論上的內容量。我們會使用 1px x 1px 的 sentinel 元素與轉換,強制包含項目的元素 (跑道) 達到想要的高度。我們會將跑道中的每個元素提升到各自的圖層,確保跑道層本身完全空白。無背景顏色如果跑道的圖層不是空白,則該圖層不符合瀏覽器最佳化的資格,且我們必須在高度有幾百千個像素的顯示卡上儲存紋理。絕對無法在行動裝置上使用。

每次捲動時,我們就會檢查可視區域是否在跑道結束時有足夠的空間。若是如此,我們將移動 Sendinel 元素,將剩餘的可視區域移至跑道底部,並填入新內容,藉此擴充跑道。

Sentinel Sentinel

對於往另一個方向捲動也是一樣。但是,我們絕不會在實作中縮減跑道,因此捲軸的位置會保持一致。

墓碑

如先前所述,我們致力讓資料來源運作在現實世界中的運作方式。具備網路延遲和一切功能。也就是說,如果使用浮動捲動功能,使用者能夠輕鬆地捲動畫面,超過我們擁有的最後一個元素。在這種情況下,我們會放置空值標記項目 (預留位置),當資料到達時,就會由項目取代。Tombstone 也屬於回收項目,並為可重複使用的 DOM 元素提供獨立的集區。因此,我們需要達成這個目標,才能順利地從空值轉變成填入內容的項目,因為這會讓使用者感到非常不悅,且可能會讓使用者失去專注力。

這塊陵墓。非常石頭,超酷的

這裡特別有趣的挑戰是,每個項目或附加圖片的文字數量不同,實際項目的高度可能大於空值標記項目。為解決這個問題,每次有資料進入時,我們就會調整目前的捲動位置,並取代可視區域上方,將捲動位置「錨定」為元素而非像素值。這個概念稱為捲動錨定。

捲動錨定

更換空值,以及調整視窗大小時 (當裝置翻轉時也會發生),同時系統會叫用捲動錨定標記。我們必須找出可視區域中最上方最可見的元素。由於該元素只能部分顯示,我們也會儲存與可視區域起始元素頂端的偏移值。

捲動錨定圖表。

如果調整可視區域的大小,且跑道有所變更,我們就可以還原在視覺上與使用者完全相同的情境。勝利!除了經過調整後的視窗以外,每個項目都有可能改變高度。那麼,我們該如何知道應該將錨定內容放在哪裡?我們絕對不會!為了找出必須調整錨定項目上方的每個元素,並加總所有高度,這可能會導致在調整大小後大幅暫停,而我們不希望如此。因此,我們假設上述每個項目的大小都與陵墓相同,並據此調整捲動位置。當元素捲動至跑道中時,我們會調整捲動位置,有效將版面配置工作延遲到實際需要時。

版面配置

我略過了一項重要的詳細資料:版面配置。每次回收 DOM 元素,通常會重新配置整個通道,這使得我們達到每秒 60 個影格的目標以下。為避免這種情況,我們自行處理版面配置的負擔,並使用絕對定位元素搭配轉換。這樣我們就能假定在跑道上進一步的所有元素仍會佔用空間。由於我們自己負責設定版面配置,因此可以快取每個項目最後的位置,以便在使用者往回捲動時,立即從快取載入正確的元素。

理想情況下,項目只有在附加至 DOM 時才會重新繪製一次,而且不會因為在伸展道中新增或移除其他項目而變得模糊。可以這麼做,但僅限於新型瀏覽器。

出血邊緣調整

最近,Chrome 新增了 CSS 元件支援功能,可讓開發人員將元素告知瀏覽器版面配置和繪製工作的範圍。由於我們在這裡自己要進行版面配置,所以這很適合用來防止隔離。每次在跑道中新增元素時,我們「知道」其他項目無需受到重組的影響。因此每個項目應該都有 contain: layout。我們也不想影響網站的其他部分,因此跑道本身也應取得這個樣式指令。

我們還考慮使用 IntersectionObservers 做為機制,偵測使用者捲動網頁是否夠多,讓我們開始回收元素並載入新資料。不過,IntersectionObserver 會指定為高延遲時間 (例如使用 requestIdleCallback),因此實際與 IntersectionObservers 相比,對回應速度可能較不快。即使是目前使用 scroll 事件的實作方式,也都會受到這個問題的影響,因為系統會「盡可能」分派捲動事件。最終,Houdini 的 Compositor Worklet 將是此問題的高擬真度解決方案。

仍不完美

我們目前的 DOM 回收實作作業會新增所有「傳遞」到可視區域的元素,而非只處理實際「顯示」畫面上的元素,因此並不是理想的做法。這表示在快速快速捲動時,會花太多錢在 Chrome 上進行版面配置和繪製作業,導致系統無法更新。完成上述步驟後,畫面只會顯示背景。重點不在於世界末日, 但絕對可以改進

如果您想透過高效能標準打造良好的使用者體驗,希望您能理解,不易遇到的簡單問題。隨著漸進式網頁應用程式成為手機的核心體驗,這將變得更加重要,網頁開發人員必須持續投入資源,以遵循效能限制的模式。

所有程式碼都可以在存放區中找到。我們已經盡可能讓這個項目可重複使用,但無法將其發布為 npm 上的實際程式庫或做為獨立的存放區。主要用途為教育性質。