原文地址:https://developers.google.com/web/updates/2016/07/infinite-scroller
原文作者:Surma
譯者:王芃
摘要: 重用你的DOM元素以及刪除那些遠(yuǎn)離可視范圍的元素人乓。為延遲顯示的元素使用占位符言疗。這里是一個無盡滾動的演示和代碼。
無盡滾動在互聯(lián)網(wǎng)上到處都有應(yīng)用。Google Music的藝術(shù)家列表是一個,F(xiàn)acebook的時間線是一個,Tweeter的話題列表也是一個。當(dāng)你向下滾動,新的內(nèi)容就神奇的“無中生有”了贮竟。這是一個得到廣泛贊揚(yáng)的、非常好的用戶體驗较剃。
在這個無盡滾動背后的技術(shù)挑戰(zhàn)其實比它看上去要難咕别。當(dāng)你想做正確的事時,你遇到的問題是巨大的写穴。開始時是一些比較簡單的事情惰拱,比如在頁面尾部的鏈接是無法點擊的,因為內(nèi)容不斷的把它們“擠”走确垫。但是問題逐漸開始變得越來越難:當(dāng)用戶將手機(jī)從豎屏改為橫屏?xí)r你該如何處理 resize
事件弓颈?或者當(dāng)列表過長時你如何避免手機(jī)的卡頓?
正確的事
我們認(rèn)為有充分的理由來實現(xiàn)一個參考設(shè)計:在保證性能的基礎(chǔ)上删掀,以一個可復(fù)用的方式來解決這些問題翔冀。
我們將會使用3種技術(shù)來達(dá)成目標(biāo):DOM回收、墓碑和滾動錨定披泪。
我們的demo會是一個類似聊天的窗口纤子,我們可以滾動這些消息列表。首先需要的是一個無盡的消息數(shù)據(jù)源款票。從技術(shù)角度看控硼,沒有任何一個無盡列表是真正無盡的,但當(dāng)有足夠的數(shù)據(jù)量填充進(jìn)去時艾少,它們看上去感覺是無盡的卡乾。為簡化問題,我們這里硬編碼了一套消息數(shù)據(jù)缚够,隨機(jī)的抽取消息幔妨、聯(lián)系人和圖片。為了更像網(wǎng)絡(luò)的真實情況谍椅,我們?nèi)藶榧尤肓艘恍┭舆t误堡。
DOM 回收
DOM回收是一個未被廣泛使用的技術(shù),它的用途是讓DOM的節(jié)點數(shù)保持在較低的數(shù)值雏吭。概括來說锁施,它的機(jī)制是利用那些離開視圖區(qū)域的、已經(jīng)創(chuàng)建的DOM元素,而不是新建DOM元素悉抵。需要承認(rèn)的一點是DOM節(jié)點本身并非耗能大戶肩狂,但是也不是一點都不消耗性能,每一個節(jié)點都會增加一些額外的內(nèi)存基跑、布局婚温、樣式和繪制。如果一個站點的DOM節(jié)點過多媳否,在低端設(shè)備上會發(fā)現(xiàn)明顯的變慢,如果沒有徹底卡死的話荆秦。同樣需要注意的一點是篱竭,在一個較大的DOM中每一次重新布局或重新應(yīng)用樣式(在節(jié)點上增加或刪除樣式所觸發(fā)的過程)的系統(tǒng)開銷都會比較昂貴。所以進(jìn)行DOM回收意味著我們會保持DOM節(jié)點在一個比較低的數(shù)量上步绸,進(jìn)而加快上面提到的這些處理過程掺逼。
第一個障礙是滾動本身。由于我們在任何時刻DOM中只有全部列表項目的一個微小子集瓤介,我們需要找到一種方式可以讓瀏覽器正確的反映出理論上應(yīng)該在“那里”的全部列表項目數(shù)量吕喘。我們這里用一個 1px * 1px
的”前哨“元素(sentinel
),并且應(yīng)用一個變換使得包含“逃兵”列表項目的元素(下圖中的 runway
)保持一個理想的高度刑桑。我們會把runaway
中的每一個元素提升到它們自己的層氯质,保持 runaway
本身是完全空的,沒有背景色祠斧,神馬都木有闻察。如果 runaway
層不是空的話,是不利于瀏覽器優(yōu)化的琢锋。因為我們將不得不在顯卡上存儲一個由成千上萬的像素組成的紋理辕漂。這樣做顯然在移動設(shè)備上是不可行的。
當(dāng)我們進(jìn)行滾動時吴超,我們會檢查是否viewport是否已經(jīng)足夠接近 runaway
的尾部钉嘹。如果是的話,我們會通過把 sentinel
和viewport中的剩余元素移向 runaway
的底部來擴(kuò)展 runaway
鲸阻,然后用新內(nèi)容渲染這些元素跋涣。
向反方向滾動時也類似,但我們無論如何也不會縮小 runaway
赘娄,原因是我們需要滾動欄的位置保持連續(xù)性仆潮。
墓碑(Tombstones)
如之前我們所說,我們會盡量讓數(shù)據(jù)源表現(xiàn)的像現(xiàn)實世界遇到的情況:有網(wǎng)絡(luò)延遲及其它情況遣臼。這就意味著如果我們的用戶飛快地滾動性置,他們會很容易就把我們渲染的有數(shù)據(jù)的項目都甩在身后。如果這種情況發(fā)生時揍堰,我們就需要放置一個墓碑條目(占位)在對應(yīng)位置鹏浅,等到數(shù)據(jù)取到后墓碑條目會被實際內(nèi)容替代嗅义。墓碑也會被回收,對于墓碑元素會有一個獨(dú)立的可復(fù)用DOM元素的池隐砸。這樣設(shè)計的原因是之碗,我們希望墓碑元素在被實際數(shù)據(jù)替代時可以有一個漂亮的過渡,而不是出現(xiàn)那種生硬的或者讓人迷失的效果季希。
這里有一個有趣的挑戰(zhàn)褪那,那就是真實的條目的高度可能會超過墓碑的高度,因為不同的文本量或者圖片的大小決定了這點式塌。為了解決這個問題博敬,每次當(dāng)取到數(shù)據(jù)后我們會調(diào)整當(dāng)前的滾動位置,而且在viewport之上的一個墓碑條目也會被替換峰尝。將滾動位置錨定到某一條目而非某一具體的像素位置偏窝,這個概念叫做滾動錨定。
滾動錨定
滾動錨定的觸發(fā)時機(jī)有兩個:一個是墓碑被替換時武学,另一個是窗口大小發(fā)生改變時(在設(shè)備發(fā)生翻轉(zhuǎn)時也會發(fā)生)祭往。我們必須要知道在viewport中的最頂部可見元素是什么。由于這個元素可能只是部分可見的火窒,所以我們也需要存儲從頂部元素到viewport頂部的偏移量硼补。
這樣的話,當(dāng)viewport改變大小時沛鸵、runaway
改變時括勺,我們是可以把場景恢復(fù)到一個看起來和原來幾乎一致的樣子。爽就一個字曲掰!但是改變大小的視窗意味著每個條目都可能改變了高度疾捍,那么我們?nèi)绾文苤涝摪彦^定的內(nèi)容移動多少偏移量呢?我們并不知道栏妖!為了搞清楚這點乱豆,我們可能不得不把錨定條目之上的元素布局起來,把它們的高度累加在一起吊趾。但顯然這樣做會造成改變大小時會有明顯的停頓宛裕,我們并不想要這樣的結(jié)果。相反论泛,我們借助于一個假設(shè):在viewport之上的每個元素都是和墓碑等高的揩尸。根據(jù)這個假設(shè)來調(diào)整對應(yīng)的滾動位置。當(dāng)元素滾動進(jìn)入 runaway
時屁奏,我們調(diào)整滾動位置岩榆,這樣就有效的把布局工作延遲到真正需要的時候了。
布局
我剛才跳過了一個重要的細(xì)節(jié):布局。每次DOM元素的回收通常情況下都會引發(fā)整個 runaway
的重新布局勇边,這會直接影響我們的性能:無法達(dá)成每秒60幀的目標(biāo)犹撒。為避免這一點,我們自己承擔(dān)了布局的重任粒褒,使用了絕對定位的元素识颊。這樣我們可以讓所有 runaway
中的元素感覺上還在占用空間,但其實那里毛都沒有奕坟。由于我們自己在操控布局祥款,我們便可以緩存每個元素消失前的位置,在用戶往回滾動時执赡,我們能立刻從緩存中加載正確的元素镰踏。
理想情況下,條目應(yīng)該只被重繪一次沙合,那就是當(dāng)它們被加到DOM時。而且應(yīng)該對于 runaway
中其它條目的增加或刪除完全不受影響跌帐。這個是可能的首懈,但是只限于現(xiàn)代瀏覽器。
極致優(yōu)化
最近谨敛,Chrome增加了CSS Containment的支持究履,這個特性允許開發(fā)者告訴瀏覽器某個元素是布局和繪制的邊界。由于我們這里采用的是自己來布局脸狸,這是一個很好的可以應(yīng)用 containment
的機(jī)會最仑。當(dāng)我們增加一個元素到 runaway
時,我們知道其它條目不應(yīng)該被這個重新布局影響炊甲。所以每個條目應(yīng)該設(shè)置一個 contain: layout
泥彤。我們同樣也不希望影響站點的其它部分,所以 runaway
本身也需要這樣設(shè)置卿啡。
另一個優(yōu)化點吟吝,我們考慮的是利用IntersectionObservers去檢測用戶是否已經(jīng)滾動了足夠距離,以便于我們決定是否開始回收DOM和加載新數(shù)據(jù)颈娜。但是 IntersectionObservers
是為高延遲設(shè)計的剑逃,所以我們實際上會“感覺”用了 IntersectionObservers
反而比不用時“響應(yīng)更慢”。在我們當(dāng)前的實現(xiàn)中滾動事件的處理其實也存在這個問題官辽。也許這個問題的可信度較高的解決方案會是 Houdini’s Compositor Worklet
仍不完美
目前的DOM回收實現(xiàn)方式仍不是完美的蛹磺,因為我們把所有“滾過”viewport的元素都添加到DOM了,而不是僅僅關(guān)心那些在屏幕上可見的元素同仆。這就意味著萤捆,如果你滾動的真的非常非常快的話,快到你堆積了大量的布局和繪制工作鳖轰,瀏覽器已經(jīng)無法跟上的地步時清酥,這時我們可能除了背景什么都看不到了。這當(dāng)然不是世界末日但是確實是一個可以優(yōu)化的地方蕴侣。
我們希望你可以看到這個過程:當(dāng)你想提供一個高性能的有良好用戶體驗的功能時焰轻,一個簡單的問題是演變成復(fù)雜問題的。隨著“Progressive Web Apps ”逐漸成為移動設(shè)備的一等公民昆雀,高性能的良好體驗會變得越來越重要辱志,開發(fā)者也必須持續(xù)的研究使用一些模式來應(yīng)對性能約束。
所有的代碼可以到這里查看狞膘,我們已經(jīng)盡力讓代碼有可復(fù)用性了揩懒,但不會發(fā)布一個npm類庫或其它單獨(dú)的項目。這個代碼的主要目的是教學(xué)挽封。
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner