[譯]無盡滾動(dòng)的復(fù)雜度--來自Google大神的拆解

原文地址:https://developers.google.com/web/updates/2016/07/infinite-scroller
原文作者:Surma
譯者:王芃


摘要: 重用你的DOM元素以及刪除那些遠(yuǎn)離可視范圍的元素。為延遲顯示的元素使用占位符。這里是一個(gè)無盡滾動(dòng)的演示代碼

無盡滾動(dòng)在互聯(lián)網(wǎng)上到處都有應(yīng)用。Google Music的藝術(shù)家列表是一個(gè),F(xiàn)acebook的時(shí)間線是一個(gè),Tweeter的話題列表也是一個(gè)。當(dāng)你向下滾動(dòng),新的內(nèi)容就神奇的“無中生有”了。這是一個(gè)得到廣泛贊揚(yáng)的、非常好的用戶體驗(yàn)。

在這個(gè)無盡滾動(dòng)背后的技術(shù)挑戰(zhàn)其實(shí)比它看上去要難。當(dāng)你想做正確的事時(shí),你遇到的問題是巨大的。開始時(shí)是一些比較簡單的事情,比如在頁面尾部的鏈接是無法點(diǎn)擊的,因?yàn)閮?nèi)容不斷的把它們“擠”走。但是問題逐漸開始變得越來越難:當(dāng)用戶將手機(jī)從豎屏改為橫屏?xí)r你該如何處理 resize 事件?或者當(dāng)列表過長時(shí)你如何避免手機(jī)的卡頓?

正確的事

我們認(rèn)為有充分的理由來實(shí)現(xiàn)一個(gè)參考設(shè)計(jì):在保證性能的基礎(chǔ)上,以一個(gè)可復(fù)用的方式來解決這些問題。

我們將會(huì)使用3種技術(shù)來達(dá)成目標(biāo):DOM回收、墓碑和滾動(dòng)錨定。

我們的demo會(huì)是一個(gè)類似聊天的窗口,我們可以滾動(dòng)這些消息列表。首先需要的是一個(gè)無盡的消息數(shù)據(jù)源。從技術(shù)角度看,沒有任何一個(gè)無盡列表是真正無盡的,但當(dāng)有足夠的數(shù)據(jù)量填充進(jìn)去時(shí),它們看上去感覺是無盡的。為簡化問題,我們這里硬編碼了一套消息數(shù)據(jù),隨機(jī)的抽取消息、聯(lián)系人和圖片。為了更像網(wǎng)絡(luò)的真實(shí)情況,我們?nèi)藶榧尤肓艘恍┭舆t。

image_1b8s8bm77scgbh31ill1qn41h199.png-786kB
image_1b8s8bm77scgbh31ill1qn41h199.png-786kB

DOM 回收

DOM回收是一個(gè)未被廣泛使用的技術(shù),它的用途是讓DOM的節(jié)點(diǎn)數(shù)保持在較低的數(shù)值。概括來說,它的機(jī)制是利用那些離開視圖區(qū)域的、已經(jīng)創(chuàng)建的DOM元素,而不是新建DOM元素。需要承認(rèn)的一點(diǎn)是DOM節(jié)點(diǎn)本身并非耗能大戶,但是也不是一點(diǎn)都不消耗性能,每一個(gè)節(jié)點(diǎn)都會(huì)增加一些額外的內(nèi)存、布局、樣式和繪制。如果一個(gè)站點(diǎn)的DOM節(jié)點(diǎn)過多,在低端設(shè)備上會(huì)發(fā)現(xiàn)明顯的變慢,如果沒有徹底卡死的話。同樣需要注意的一點(diǎn)是,在一個(gè)較大的DOM中每一次重新布局或重新應(yīng)用樣式(在節(jié)點(diǎn)上增加或刪除樣式所觸發(fā)的過程)的系統(tǒng)開銷都會(huì)比較昂貴。所以進(jìn)行DOM回收意味著我們會(huì)保持DOM節(jié)點(diǎn)在一個(gè)比較低的數(shù)量上,進(jìn)而加快上面提到的這些處理過程。

第一個(gè)障礙是滾動(dòng)本身。由于我們在任何時(shí)刻DOM中只有全部列表項(xiàng)目的一個(gè)微小子集,我們需要找到一種方式可以讓瀏覽器正確的反映出理論上應(yīng)該在“那里”的全部列表項(xiàng)目數(shù)量。我們這里用一個(gè) 1px * 1px 的”前哨“元素(sentinel),并且應(yīng)用一個(gè)變換使得包含“逃兵”列表項(xiàng)目的元素(下圖中的 runway)保持一個(gè)理想的高度。我們會(huì)把runaway中的每一個(gè)元素提升到它們自己的層,保持 runaway 本身是完全空的,沒有背景色,神馬都木有。如果 runaway層不是空的話,是不利于瀏覽器優(yōu)化的。因?yàn)槲覀儗⒉坏貌辉陲@卡上存儲一個(gè)由成千上萬的像素組成的紋理。這樣做顯然在移動(dòng)設(shè)備上是不可行的。

當(dāng)我們進(jìn)行滾動(dòng)時(shí),我們會(huì)檢查是否viewport是否已經(jīng)足夠接近 runaway 的尾部。如果是的話,我們會(huì)通過把 sentinel和viewport中的剩余元素移向 runaway的底部來擴(kuò)展 runaway,然后用新內(nèi)容渲染這些元素。

向反方向滾動(dòng)時(shí)也類似,但我們無論如何也不會(huì)縮小 runaway,原因是我們需要滾動(dòng)欄的位置保持連續(xù)性。

墓碑(Tombstones)

如之前我們所說,我們會(huì)盡量讓數(shù)據(jù)源表現(xiàn)的像現(xiàn)實(shí)世界遇到的情況:有網(wǎng)絡(luò)延遲及其它情況。這就意味著如果我們的用戶飛快地滾動(dòng),他們會(huì)很容易就把我們渲染的有數(shù)據(jù)的項(xiàng)目都甩在身后。如果這種情況發(fā)生時(shí),我們就需要放置一個(gè)墓碑條目(占位)在對應(yīng)位置,等到數(shù)據(jù)取到后墓碑條目會(huì)被實(shí)際內(nèi)容替代。墓碑也會(huì)被回收,對于墓碑元素會(huì)有一個(gè)獨(dú)立的可復(fù)用DOM元素的池。這樣設(shè)計(jì)的原因是,我們希望墓碑元素在被實(shí)際數(shù)據(jù)替代時(shí)可以有一個(gè)漂亮的過渡,而不是出現(xiàn)那種生硬的或者讓人迷失的效果。

墓碑元素
墓碑元素

這里有一個(gè)有趣的挑戰(zhàn),那就是真實(shí)的條目的高度可能會(huì)超過墓碑的高度,因?yàn)椴煌奈谋玖炕蛘邎D片的大小決定了這點(diǎn)。為了解決這個(gè)問題,每次當(dāng)取到數(shù)據(jù)后我們會(huì)調(diào)整當(dāng)前的滾動(dòng)位置,而且在viewport之上的一個(gè)墓碑條目也會(huì)被替換。將滾動(dòng)位置錨定到某一條目而非某一具體的像素位置,這個(gè)概念叫做滾動(dòng)錨定。

滾動(dòng)錨定

滾動(dòng)錨定的觸發(fā)時(shí)機(jī)有兩個(gè):一個(gè)是墓碑被替換時(shí),另一個(gè)是窗口大小發(fā)生改變時(shí)(在設(shè)備發(fā)生翻轉(zhuǎn)時(shí)也會(huì)發(fā)生)。我們必須要知道在viewport中的最頂部可見元素是什么。由于這個(gè)元素可能只是部分可見的,所以我們也需要存儲從頂部元素到viewport頂部的偏移量。

滾動(dòng)錨定
滾動(dòng)錨定

這樣的話,當(dāng)viewport改變大小時(shí)、runaway 改變時(shí),我們是可以把場景恢復(fù)到一個(gè)看起來和原來幾乎一致的樣子。爽就一個(gè)字!但是改變大小的視窗意味著每個(gè)條目都可能改變了高度,那么我們?nèi)绾文苤涝摪彦^定的內(nèi)容移動(dòng)多少偏移量呢?我們并不知道!為了搞清楚這點(diǎn),我們可能不得不把錨定條目之上的元素布局起來,把它們的高度累加在一起。但顯然這樣做會(huì)造成改變大小時(shí)會(huì)有明顯的停頓,我們并不想要這樣的結(jié)果。相反,我們借助于一個(gè)假設(shè):在viewport之上的每個(gè)元素都是和墓碑等高的。根據(jù)這個(gè)假設(shè)來調(diào)整對應(yīng)的滾動(dòng)位置。當(dāng)元素滾動(dòng)進(jìn)入 runaway 時(shí),我們調(diào)整滾動(dòng)位置,這樣就有效的把布局工作延遲到真正需要的時(shí)候了。

布局

我剛才跳過了一個(gè)重要的細(xì)節(jié):布局。每次DOM元素的回收通常情況下都會(huì)引發(fā)整個(gè) runaway 的重新布局,這會(huì)直接影響我們的性能:無法達(dá)成每秒60幀的目標(biāo)。為避免這一點(diǎn),我們自己承擔(dān)了布局的重任,使用了絕對定位的元素。這樣我們可以讓所有 runaway 中的元素感覺上還在占用空間,但其實(shí)那里毛都沒有。由于我們自己在操控布局,我們便可以緩存每個(gè)元素消失前的位置,在用戶往回滾動(dòng)時(shí),我們能立刻從緩存中加載正確的元素。

理想情況下,條目應(yīng)該只被重繪一次,那就是當(dāng)它們被加到DOM時(shí)。而且應(yīng)該對于 runaway 中其它條目的增加或刪除完全不受影響。這個(gè)是可能的,但是只限于現(xiàn)代瀏覽器。

極致優(yōu)化

最近,Chrome增加了CSS Containment的支持,這個(gè)特性允許開發(fā)者告訴瀏覽器某個(gè)元素是布局和繪制的邊界。由于我們這里采用的是自己來布局,這是一個(gè)很好的可以應(yīng)用 containment 的機(jī)會(huì)。當(dāng)我們增加一個(gè)元素到 runaway時(shí),我們知道其它條目不應(yīng)該被這個(gè)重新布局影響。所以每個(gè)條目應(yīng)該設(shè)置一個(gè) contain: layout。我們同樣也不希望影響站點(diǎn)的其它部分,所以 runaway 本身也需要這樣設(shè)置。

另一個(gè)優(yōu)化點(diǎn),我們考慮的是利用IntersectionObservers去檢測用戶是否已經(jīng)滾動(dòng)了足夠距離,以便于我們決定是否開始回收DOM和加載新數(shù)據(jù)。但是 IntersectionObservers 是為高延遲設(shè)計(jì)的,所以我們實(shí)際上會(huì)“感覺”用了 IntersectionObservers 反而比不用時(shí)“響應(yīng)更慢”。在我們當(dāng)前的實(shí)現(xiàn)中滾動(dòng)事件的處理其實(shí)也存在這個(gè)問題。也許這個(gè)問題的可信度較高的解決方案會(huì)是 Houdini’s Compositor Worklet

仍不完美

目前的DOM回收實(shí)現(xiàn)方式仍不是完美的,因?yàn)槲覀儼阉小皾L過”viewport的元素都添加到DOM了,而不是僅僅關(guān)心那些在屏幕上可見的元素。這就意味著,如果你滾動(dòng)的真的非常非常快的話,快到你堆積了大量的布局和繪制工作,瀏覽器已經(jīng)無法跟上的地步時(shí),這時(shí)我們可能除了背景什么都看不到了。這當(dāng)然不是世界末日但是確實(shí)是一個(gè)可以優(yōu)化的地方。

我們希望你可以看到這個(gè)過程:當(dāng)你想提供一個(gè)高性能的有良好用戶體驗(yàn)的功能時(shí),一個(gè)簡單的問題是演變成復(fù)雜問題的。隨著“Progressive Web Apps ”逐漸成為移動(dòng)設(shè)備的一等公民,高性能的良好體驗(yàn)會(huì)變得越來越重要,開發(fā)者也必須持續(xù)的研究使用一些模式來應(yīng)對性能約束。

所有的代碼可以到這里查看,我們已經(jīng)盡力讓代碼有可復(fù)用性了,但不會(huì)發(fā)布一個(gè)npm類庫或其它單獨(dú)的項(xiàng)目。這個(gè)代碼的主要目的是教學(xué)。

慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,792評論 1 92
  • 1. 介紹 瀏覽器可能是最廣泛使用的軟件。本書將介紹瀏覽器的工作原理。我們將看到,當(dāng)你在地址欄中輸入google....
    康斌閱讀 2,048評論 7 18
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評論 25 708
  • 一個(gè)人在生活中可能有太多的不順心和太多的誘惑,樂極生悲和痛不欲生的人我們實(shí)在看得太多了。雖然僅二十余歲,但...
    紅發(fā)香克斯_閱讀 192評論 0 0
  • 細(xì)雨過后,一時(shí)的興致?lián)沃愎淦鹆讼嗵幜巳甑男@。一時(shí)迎來周圍人數(shù)雙巡視的目光。應(yīng)該是很久沒見過這么充滿童趣的學(xué)...
    趙凡一閱讀 371評論 3 5