[譯] 無盡滾動的複雜度 -- 來自 Google 大神的拆解

原文地址:developers.google.com/web/updates…
原文做者:Surma
譯者:王芃css


摘要: 重用你的DOM元素以及刪除那些遠離可視範圍的元素。爲延遲顯示的元素使用佔位符。這裏是一個無盡滾動的演示代碼git

無盡滾動在互聯網上處處都有應用。Google Music的藝術家列表是一個,Facebook的時間線是一個,Tweeter的話題列表也是一個。當你向下滾動,新的內容就神奇的「無中生有」了。這是一個獲得普遍讚賞的、很是好的用戶體驗。github

在這個無盡滾動背後的技術挑戰其實比它看上去要難。當你想作正確的事時,你遇到的問題是巨大的。開始時是一些比較簡單的事情,好比在頁面尾部的連接是沒法點擊的,由於內容不斷的把它們「擠」走。可是問題逐漸開始變得愈來愈難:當用戶將手機從豎屏改成橫屏時你該如何處理 resize 事件?或者當列表過長時你如何避免手機的卡頓?web

正確的事

咱們認爲有充分的理由來實現一個參考設計:在保證性能的基礎上,以一個可複用的方式來解決這些問題。chrome

咱們將會使用3種技術來達成目標:DOM回收、墓碑和滾動錨定。npm

咱們的demo會是一個相似聊天的窗口,咱們能夠滾動這些消息列表。首先須要的是一個無盡的消息數據源。從技術角度看,沒有任何一個無盡列表是真正無盡的,但當有足夠的數據量填充進去時,它們看上去感受是無盡的。爲簡化問題,咱們這裏硬編碼了一套消息數據,隨機的抽取消息、聯繫人和圖片。爲了更像網絡的真實狀況,咱們人爲加入了一些延遲。瀏覽器

image_1b8s8bm77scgbh31ill1qn41h199.png-786kB

DOM 回收

DOM回收是一個未被普遍使用的技術,它的用途是讓DOM的節點數保持在較低的數值。歸納來講,它的機制是利用那些離開視圖區域的、已經建立的DOM元素,而不是新建DOM元素。須要認可的一點是DOM節點自己並不是耗能大戶,可是也不是一點都不消耗性能,每個節點都會增長一些額外的內存、佈局、樣式和繪製。若是一個站點的DOM節點過多,在低端設備上會發現明顯的變慢,若是沒有完全卡死的話。一樣須要注意的一點是,在一個較大的DOM中每一次從新佈局或從新應用樣式(在節點上增長或刪除樣式所觸發的過程)的系統開銷都會比較昂貴。因此進行DOM回收意味着咱們會保持DOM節點在一個比較低的數量上,進而加快上面提到的這些處理過程。緩存

第一個障礙是滾動自己。因爲咱們在任什麼時候刻DOM中只有所有列表項目的一個微小子集,咱們須要找到一種方式可讓瀏覽器正確的反映出理論上應該在「那裏」的所有列表項目數量。咱們這裏用一個 1px * 1px 的」前哨「元素(sentinel),而且應用一個變換使得包含「逃兵」列表項目的元素(下圖中的 runway)保持一個理想的高度。咱們會把runaway中的每個元素提高到它們本身的層,保持 runaway 自己是徹底空的,沒有背景色,神馬都木有。若是 runaway層不是空的話,是不利於瀏覽器優化的。由於咱們將不得不在顯卡上存儲一個由成千上萬的像素組成的紋理。這樣作顯然在移動設備上是不可行的。網絡

當咱們進行滾動時,咱們會檢查是否viewport是否已經足夠接近 runaway 的尾部。若是是的話,咱們會經過把 sentinel和viewport中的剩餘元素移向 runaway的底部來擴展 runaway,而後用新內容渲染這些元素。dom

向反方向滾動時也相似,但咱們不管如何也不會縮小 runaway,緣由是咱們須要滾動欄的位置保持連續性。

墓碑(Tombstones)

如以前咱們所說,咱們會盡可能讓數據源表現的像現實世界遇到的狀況:有網絡延遲及其它狀況。這就意味着若是咱們的用戶飛快地滾動,他們會很容易就把咱們渲染的有數據的項目都甩在身後。若是這種狀況發生時,咱們就須要放置一個墓碑條目(佔位)在對應位置,等到數據取到後墓碑條目會被實際內容替代。墓碑也會被回收,對於墓碑元素會有一個獨立的可複用DOM元素的池。這樣設計的緣由是,咱們但願墓碑元素在被實際數據替代時能夠有一個漂亮的過渡,而不是出現那種生硬的或者讓人迷失的效果。

墓碑元素

這裏有一個有趣的挑戰,那就是真實的條目的高度可能會超過墓碑的高度,由於不一樣的文本量或者圖片的大小決定了這點。爲了解決這個問題,每次當取到數據後咱們會調整當前的滾動位置,並且在viewport之上的一個墓碑條目也會被替換。將滾動位置錨定到某一條目而非某一具體的像素位置,這個概念叫作滾動錨定。

滾動錨定

滾動錨定的觸發時機有兩個:一個是墓碑被替換時,另外一個是窗口大小發生改變時(在設備發生翻轉時也會發生)。咱們必需要知道在viewport中的最頂部可見元素是什麼。因爲這個元素可能只是部分可見的,因此咱們也須要存儲從頂部元素到viewport頂部的偏移量。

滾動錨定

這樣的話,當viewport改變大小時、runaway 改變時,咱們是能夠把場景恢復到一個看起來和原來幾乎一致的樣子。爽就一個字!可是改變大小的視窗意味着每一個條目均可能改變了高度,那麼咱們如何能知道該把錨定的內容移動多少偏移量呢?咱們並不知道!爲了搞清楚這點,咱們可能不得不把錨定條目之上的元素佈局起來,把它們的高度累加在一塊兒。但顯然這樣作會形成改變大小時會有明顯的停頓,咱們並不想要這樣的結果。相反,咱們藉助於一個假設:在viewport之上的每一個元素都是和墓碑等高的。根據這個假設來調整對應的滾動位置。當元素滾動進入 runaway 時,咱們調整滾動位置,這樣就有效的把佈局工做延遲到真正須要的時候了。

佈局

我剛纔跳過了一個重要的細節:佈局。每次DOM元素的回收一般狀況下都會引起整個 runaway 的從新佈局,這會直接影響咱們的性能:沒法達成每秒60幀的目標。爲避免這一點,咱們本身承擔了佈局的重任,使用了絕對定位的元素。這樣咱們可讓全部 runaway 中的元素感受上還在佔用空間,但其實那裏毛都沒有。因爲咱們本身在操控佈局,咱們即可以緩存每一個元素消失前的位置,在用戶往回滾動時,咱們能馬上從緩存中加載正確的元素。

理想狀況下,條目應該只被重繪一次,那就是當它們被加到DOM時。並且應該對於 runaway 中其它條目的增長或刪除徹底不受影響。這個是可能的,可是隻限於現代瀏覽器。

極致優化

最近,Chrome增長了CSS Containment的支持,這個特性容許開發者告訴瀏覽器某個元素是佈局和繪製的邊界。因爲咱們這裏採用的是本身來佈局,這是一個很好的能夠應用 containment 的機會。當咱們增長一個元素到 runaway時,咱們知道其它條目不該該被這個從新佈局影響。因此每一個條目應該設置一個 contain: layout。咱們一樣也不但願影響站點的其它部分,因此 runaway 自己也須要這樣設置。

另外一個優化點,咱們考慮的是利用IntersectionObservers去檢測用戶是否已經滾動了足夠距離,以便於咱們決定是否開始回收DOM和加載新數據。可是 IntersectionObservers 是爲高延遲設計的,因此咱們實際上會「感受」用了 IntersectionObservers 反而比不用時「響應更慢」。在咱們當前的實現中滾動事件的處理其實也存在這個問題。也許這個問題的可信度較高的解決方案會是 Houdini’s Compositor Worklet

仍不完美

目前的DOM回收實現方式仍不是完美的,由於咱們把全部「滾過」viewport的元素都添加到DOM了,而不是僅僅關心那些在屏幕上可見的元素。這就意味着,若是你滾動的真的很是很是快的話,快到你堆積了大量的佈局和繪製工做,瀏覽器已經沒法跟上的地步時,這時咱們可能除了背景什麼都看不到了。這固然不是世界末日可是確實是一個能夠優化的地方。

咱們但願你能夠看到這個過程:當你想提供一個高性能的有良好用戶體驗的功能時,一個簡單的問題是演變成複雜問題的。隨着「Progressive Web Apps 」逐漸成爲移動設備的一等公民,高性能的良好體驗會變得愈來愈重要,開發者也必須持續的研究使用一些模式來應對性能約束。

全部的代碼能夠到這裏查看,咱們已經盡力讓代碼有可複用性了,但不會發佈一個npm類庫或其它單獨的項目。這個代碼的主要目的是教學。

相關文章
相關標籤/搜索