重點摘要:重複使用 DOM 元素,並移除離可視區域很遠的元素。使用預留位置來處理延遲的資料。以下是無限捲軸的示範和程式碼。
無限捲動介面在網路上隨處可見。Google Music 的藝人名單、Facebook 的時間軸,以及 Twitter 的即時動態都是一個。您向下捲動頁面,在到達底部之前,新內容就會神奇地出現,似乎是從無到有。這能為使用者提供流暢的體驗,並讓他們輕鬆體驗產品的魅力。
不過,無限捲動功能背後的技術挑戰比想像中更難克服。在您想做對的事時,會遇到各種問題。這類問題的起因可能很簡單,例如底部的連結實際上無法點選,因為內容會不斷將底部推開。但問題會變得更難解決。當使用者將手機從直向轉為橫向時,您如何處理調整大小事件?或者,當清單過長時,您如何防止手機停止運作?
The right thing™
我們認為這項原因已足以推出參考實作項目,以便說明如何以可重複使用的方式解決所有這些問題,同時維持效能標準。
我們將使用 3 種技巧來達成目標:DOM 回收、墓碑和捲動錨點。
我們的示範案例將會是類似 Hangouts 的即時通訊視窗,我們可以捲動瀏覽訊息。首先,我們需要無限量的即時通訊訊息來源。從技術層面來說,目前沒有任何無限捲動器是「真正」無限的,但由於這些捲動器可處理的資料量,因此也算是無限的。為了簡化操作,我們會將一組即時通訊訊息硬式編碼,並隨機挑選訊息、作者和偶爾附加的圖片,再加上一點人工延遲,讓系統的行為更接近真實網路。

DOM 回收
DOM 回收是一種未充分利用的技術,可減少 DOM 節點數量。一般來說,您應該使用已建立的 DOM 元素,而非建立新的元素。誠然,DOM 節點本身的成本不高,但並非免費,因為每個節點都會在記憶體、版面配置、樣式和繪製方面增加額外成本。如果網站的 DOM 太大,低階裝置的速度會明顯變慢,甚至完全無法使用。請注意,每當您重新配置和重新套用樣式時 (每當在節點中新增或移除類別時就會觸發此程序),DOM 越大,所需的資源就越多。回收 DOM 節點代表我們會大幅降低 DOM 節點的總數,讓所有這些程序更快速。
第一個障礙是捲動本身。由於在任何特定時間,DOM 中都只有所有可用項目的一小部分,因此我們需要找出其他方法,讓瀏覽器的捲軸正確反映理論上存在的內容量。我們會使用 1 個 1 像素乘以 1 像素的哨兵元素,搭配轉換功能,強制包含項目的元素 (跑道) 具有所需高度。我們會將跑道中的每個元素升級至各自的圖層,確保跑道本身的圖層完全空白。沒有背景顏色,什麼都沒有。如果跑道的圖層非空白,就無法使用瀏覽器的最佳化功能,因此我們必須在顯示卡上儲存高度為幾十萬像素的紋理。絕對不適合在行動裝置上使用。
每次捲動時,我們都會檢查視區是否已接近跑道盡頭。如果是,我們會移動哨兵元素,並將已離開檢視區的項目移至跑道底部,然後以新內容填入這些項目,藉此延長跑道。
在其他方向捲動也是一樣。不過,我們絕不會在實作中縮小跑道,以便保持捲軸位置一致。
空值標記
如先前所述,我們會盡量讓資料來源的運作方式與現實世界中的某些事物相似。網路延遲和其他因素。也就是說,如果使用者使用快速捲動功能,他們可以輕鬆捲動至我們有資料的最後一個元素。在這種情況下,我們會放置墓碑項目 (預留位置),並在資料到達後,將其替換為含有實際內容的項目。系統也會回收墓碑,並為可重複使用的 DOM 元素建立專屬的集區。我們需要這麼做,才能從空白區塊順利轉換至填入內容的項目,否則使用者會感到非常不協調,甚至可能會失去焦點。

這裡有個有趣的挑戰,就是實際項目的高度可能會比空白項目更高,因為每個項目的文字或附加圖片數量不同。為解決這個問題,我們會在每次接收資料並在檢視區上方替換墓碑時,調整目前的捲動位置,並錨定捲動位置至元素,而非像素值。這個概念稱為捲動錨定。
捲動錨定
在替換墓碑和調整視窗大小時,系統都會叫用我們的捲動錨點 (在裝置翻轉時也會發生這種情況)。我們必須找出可視區域中位於最上方的可見元素。由於該元素只能部分顯示,我們也會儲存從可視區域開始的元素頂端偏移量。

如果視區大小調整,且跑道發生變化,我們就能還原使用者在視覺上感覺相同的情況。勝出!除了大小調整的視窗,每個項目的高度都可能有所變動,因此我們如何得知應將錨定內容放在多遠的位置?我們不會!為了找出這個值,我們必須在錨定項目上方排列每個元素,並將所有元素的高度加總起來;這可能會導致在調整大小後出現明顯的暫停情形,而我們不希望發生這種情況。我們會假設上述每個項目的大小都與墓碑圖示相同,並據此調整捲動位置。當元素捲動至跑道時,我們會調整捲動位置,有效地將版面配置工作延後至實際需要時才執行。
版面配置
我跳過了一個重要的細節:版面配置。每次回收 DOM 元素通常會重新排版整個跑道,這會讓我們遠低於每秒 60 個影格的目標。為避免這種情況,我們會將版面配置的負擔轉移到自己身上,並使用含有轉換的絕對定位元素。這樣一來,我們可以假設在跑道更遠處的所有元素仍會佔用空間,但實際上只有空白空間。由於我們會自行進行版面配置,因此可以快取每個項目結束的位置,並在使用者向後捲動時,立即從快取中載入正確的元素。
理想情況下,項目附加到 DOM 後只會重新繪製一次,且不會受到跑道中其他項目的新增或移除作業影響。這麼做是可行的,但僅適用於新式瀏覽器。
最新調整
近期,Chrome 新增了 CSS 容器功能,可讓開發人員告知瀏覽器某個元素是版面配置和繪製作業的邊界。由於我們在此處自行執行版面配置,因此這是用於容器的最佳應用程式。每當我們在跑道中新增元素時,我們都知道其他項目不需要受到重新配置的影響。因此,每個項目都應取得 contain: layout
。我們也不希望影響網站的其他部分,因此跑道本身也應取得此樣式指示。
我們考慮的另一個做法是使用 IntersectionObservers
做為機制,偵測使用者捲動到足以開始回收元素及載入新資料的程度。不過,IntersectionObserver 會指定為高延遲 (就像使用 requestIdleCallback
一樣),因此使用 IntersectionObserver 的回應速度可能會比不使用時感覺較慢。即使是目前使用 scroll
事件的實作項目,也會遇到這個問題,因為捲動事件是依據「盡力」原則調度。Houdini 的 Compositor Worklet 將是解決此問題的高保真解決方案。
但仍不夠完美
我們目前實作 DOM 回收功能的方式並不理想,因為它會新增所有透過檢視區「通過」的元素,而非只處理實際「位於」螢幕上的元素。也就是說,當您快速捲動時,Chrome 會為版面配置和繪製工作耗費大量資源,導致無法跟上速度。您只會看到背景。這並非世界末日,但確實需要改善。
我們希望您能瞭解,當您想同時提供絕佳的使用者體驗和高效能標準時,簡單的問題可能會變得多麼棘手。隨著漸進式網頁應用程式成為行動裝置上的核心體驗,這項做法的重要性將會提高,網頁開發人員必須持續投入,使用符合效能限制的模式。
您可以在我們的存放區中找到所有程式碼。我們已盡力讓其可重複使用,但不會將其做為實際的程式庫在 npm 上發布,或做為獨立的存放區。主要用途是教育用途。