UX Planet論壇上有過這么一篇熱門文章: Infinite Scrolling Best Practices,它從UX角度分析了無限滾動加載的設(shè)計實(shí)踐。
無限滾動加載在互聯(lián)網(wǎng)上到處都有應(yīng)用:
豆瓣首頁是一個,F(xiàn)acebook的Timeline是一個,Tweeter的話題列表也是一個。當(dāng)你向下滾動,新的內(nèi)容就神奇的“無中生有”了。這是一個得到廣泛贊揚(yáng)的用戶體驗(yàn)。
無限滾動加載背后的技術(shù)挑戰(zhàn)其實(shí)比想象中要多不少。尤其是要考慮頁面性能,需要做到極致。
本文通過代碼實(shí)例,來實(shí)現(xiàn)一個無限滾動加載效果。更重要的是,在實(shí)現(xiàn)過程中,對于頁面性能的分析和處理力圖做到最大化,希望對讀者有所啟發(fā),同時也歡迎與我討論。
性能測量
在開啟我們的代碼之前,有必要先了解一下常用的性能測量手段:
1)使用window.performance
HTML5帶來的performance API功能強(qiáng)大。我們可以使用其performance.now()精確計算程序執(zhí)行時間。performance.now()與Date.now()不同的是,返回了以微秒(百萬分之一秒)為單位的時間,更加精準(zhǔn)。并且與 Date.now() 會受系統(tǒng)程序執(zhí)行阻塞的影響不同,performance.now() 的時間是以恒定速率遞增的,不受系統(tǒng)時間的影響(系統(tǒng)時間可被人為或軟件調(diào)整)。
同時,也可以使用performance.mark()標(biāo)記各種時間戳(就像在地圖上打點(diǎn)),保存為各種測量值(測量地圖上的點(diǎn)之間的距離),便可以批量地分析這些數(shù)據(jù)了。
2)使用console.time方法與console.timeEnd方法
其中console.time方法用于標(biāo)記開始時間,console.timeEnd方法用于標(biāo)記結(jié)束時間,并且將結(jié)束時間與開始時間之間經(jīng)過的毫秒數(shù)在控制臺中輸出。
3)使用專業(yè)的測量工具/平臺:jsPerf
這次實(shí)現(xiàn)中,我們使用第二種方法,因?yàn)樗呀?jīng)完全可以滿足我們的需求,且兼容性更加全面。
整體思路和方案設(shè)計
我們要實(shí)現(xiàn)的頁面樣例如圖,

它能夠做到無限下拉加載內(nèi)容。我把紅線標(biāo)出的部分叫做一個block-item,后續(xù)也都用這種命名。
1)關(guān)于設(shè)計方案,肯定第一個最基本、最樸素的思想是下拉到底部之后發(fā)送ajax異步請求,成功之后的回調(diào)里進(jìn)行頁面拼接。
2)但是觀察頁面布局,很明顯圖片較多,每一個block-item區(qū)塊都有一張配圖。當(dāng)加載后的內(nèi)容插入到頁面中時,瀏覽器就開始獲取圖片。這意味著所有的圖像同時下載,瀏覽器中的下載通道將被占滿。同時,由于內(nèi)容優(yōu)先于用戶瀏覽而加載,所以可能被迫下載底部那些永遠(yuǎn)也不會被用戶瀏覽到的圖像。
所以,我們需要設(shè)計一個懶加載效果,使得頁面速度更快,并且節(jié)省用戶的流量費(fèi)用和延長電池壽命。
3)上一條提到的懶加載實(shí)現(xiàn)上,為了避免到真正的頁面底部時才進(jìn)行加載和渲染,而造成用戶較長時間等待。我們可以設(shè)置一個合理閾值,在用戶滾動到頁面底部之前,先進(jìn)行提前加載。
4)另外,頁面滾動的事件肯定是需要監(jiān)聽的。同時,頁面滾動問題也比較棘手,后面將專為滾動進(jìn)行分析。
5)DOM操作我們知道是及其緩慢而低效的,有興趣的同學(xué)可以研究一下jsPerf上一些經(jīng)典的benchmark,比如這篇。關(guān)于造成這種緩慢的原因,社區(qū)上同樣有很多文章有過分析,這里就不再深入。但我想總結(jié)并補(bǔ)充的是:DOM操作,光是為了找一個節(jié)點(diǎn),就從本質(zhì)上比簡單的檢索內(nèi)存中的值要慢。一些DOM操作還需要重新計算樣式來讀取或檢索一個值。更突出的問題在于:DOM操作是阻塞的,所以當(dāng)有一個DOM操作在進(jìn)行時,其他的什么都不能做,包括用戶與頁面的交互(除了滾動)。這是一個極度傷害用戶體驗(yàn)的事實(shí)。
所以,在下面的效果實(shí)現(xiàn)中,我采用了大量“不可思議”的DOM緩存,甚至極端的緩存everything。當(dāng)然,這樣做的收益也在最后部分有所展現(xiàn)。
滾動問題
滾動問題不難想象在于高頻率的觸發(fā)滾動事件處理上。具我親測,在極端case下,滾動及其卡頓。即使?jié)L動不卡頓,你可以打開Chrome控制臺發(fā)現(xiàn),幀速率也非常慢。關(guān)于幀速率的問題,我們有著名的16.7毫秒理論。關(guān)于這個時間分析,社區(qū)上也有不少文章闡述,這里不再展開。
針對于此,有很多讀者會立刻想到“截流和防抖動函數(shù)”(Throttle和Debounce)。
簡單總結(jié)一下:
1)Throttle允許我們限制激活響應(yīng)的數(shù)量。我們可以限制每秒回調(diào)的數(shù)量。反過來,也就是說在激活下一個回調(diào)之前要等待多少時間;
2)Debounce意味著當(dāng)事件發(fā)生時,我們不會立即激活回調(diào)。相反,我們等待一定的時間并檢查相同的事件是否再次觸發(fā)。如果是,我們重置定時器,并再次等待。如果在等待期間沒有發(fā)生相同的事件,我們就立即激活回調(diào)。
具體這里就不代碼實(shí)現(xiàn)了。原理明白之后,應(yīng)該不難寫出。
但是我這里想從移動端主要瀏覽器處理滾動的方式入手,來思考這個問題:
1)在Android機(jī)器上,用戶滾動屏幕時,滾動事件高頻率發(fā)生——在Galaxy-SIII手機(jī)上,大約頻率是一秒一百次。這意味著,滾動處理函數(shù)也被調(diào)用了數(shù)百次,而這些又都是成本較大的函數(shù)。
2)在Safari瀏覽器上,我們遇到的問題恰恰是相反的:用戶每次滾動屏幕時,滾動事件只在滾動動畫停止時才觸發(fā)。當(dāng)用戶在iPhone上滾動屏幕時,不會運(yùn)行更新界面的代碼(滾動停止時才會運(yùn)行一次)。
另外,我想也許會有讀者想到rAf(requestAnimationFrame),但是據(jù)我觀察,很多前端其實(shí)并不明白requestAnimationFrame技術(shù)的原理和解決的問題。只是機(jī)械地把動畫性能、掉幀問題甩到這么一個名詞上。在真實(shí)項(xiàng)目中,也沒有親自實(shí)現(xiàn)過,更不要說考慮requestAnimationFrame的兼容性情況了。這里場景我并不會使用rAf,因?yàn)?。setTimeout的定時器值推薦最小使用16.7ms(原因請去社區(qū)上找答案,不再細(xì)講),我們這里并不會超過這個限制,并且考慮兼容性。關(guān)于這項(xiàng)技術(shù)的使用,如果有問題,歡迎留言討論。
基于以上,我的解決方案是既不同于Throttle,也不同于Debounce,但是和這兩個思想,尤其是Throttle又比較類似:把滾動事件替換為一個帶有計時器的滾動處理程序,每100毫秒進(jìn)行簡單檢查,看這段時間內(nèi)用戶是否滾動過。如果沒有,則什么都不做;如果有,就進(jìn)行處理。
用戶體驗(yàn)優(yōu)化小竅門
在圖像加載完成時,使用淡入(fade in)效果出現(xiàn)。這在實(shí)際情況上會稍微慢一下,應(yīng)該慢一個過渡執(zhí)行時間。但用戶體驗(yàn)上感覺會更快。這是已經(jīng)被證實(shí)且普遍應(yīng)用的小“trick”。但是據(jù)我感覺,它確實(shí)有效。我們的代碼實(shí)現(xiàn)也采用了這個小竅門。不過類似這種“社會心理學(xué)”范疇的東西,顯然不是本文研究的重點(diǎn)。
總結(jié)一下
代碼上將會采用:超前閾值的懶加載+DOM Cache和圖片Cache+滾動throttle模擬+CSS fadeIn動畫。
具體功能封裝上和一些實(shí)現(xiàn)層面的東西,請您繼續(xù)閱讀。
代碼實(shí)現(xiàn)
DOM結(jié)構(gòu)
整體結(jié)構(gòu)如下:
<div class="exp-list-box" id="expListBox">
<ul class="exp-list" id="expList">
</ul>
<div class="ui-refresh-down"></div>
</div>
主體內(nèi)容放在id為“expListBox”的container里面,id為“expList”的ul是頁面加載內(nèi)容的容器。
因?yàn)槊看渭虞d并append進(jìn)入HTML的內(nèi)容相對較多。我使用了模版來取代傳統(tǒng)的字符串拼接。前端模版這次選用了我的同事顏海鏡大神的開源作品,模版結(jié)構(gòu)為:
<#dataList.forEach(function (v) {#>
<div id="s-<#=v.eid#>" class="slide">
<li>
<a href="<#=v.href#>">
<img class="img" src="data:image/gif;base64,R0lGODdhAQABAPAAAP%2F%2F%2FwAAACwAAAAAAQABAEACAkQBADs%3D"
data-src="<#=v.src#>">
</img>
<strong><#=v.title#></strong>
<span class="writer"><#=v.writer#></span>
<span class="good-num"><#=v.succNum#></span>
</a>
</li>
</div>
<#})#>
以上模版內(nèi)容由每次ajax請求到的數(shù)據(jù)填充,并添加進(jìn)入頁面,構(gòu)成每個block-item。
這里需要注意觀察,有助于對后面邏輯的理解。頁面中一個block-item下div屬性存有該block-item的eid值,對應(yīng)class叫做"slide",子孫節(jié)點(diǎn)包含有一個image標(biāo)簽,src初始賦值為1px的空白圖進(jìn)行占位。真實(shí)圖片資源位置存儲在"data-src"中。
另外,請求返回的數(shù)據(jù)dataList可以理解為由9個對象構(gòu)成的數(shù)組,也就是說,每次請求加載9個block-item。
樣式亮點(diǎn)
樣式方面不是這篇文章的重點(diǎn),挑選最核心的一行來說明一下:
.slide .img{
display: inline-block;
width: 90px;
height: 90px;
margin: 0 auto;
opacity: 0;
-webkit-transition: opacity 0.25s ease-in-out;
-moz-transition: opacity 0.25s ease-in-out;
-o-transition: opacity 0.25s ease-in-out;
transition: opacity 0.25s ease-in-out;
}
唯一需要注意的是image的opacity設(shè)置為0,圖片將會在成功請求并渲染后調(diào)整為1,輔助transition屬性實(shí)現(xiàn)一個fade in效果。
對應(yīng)我們上面所提到的那個“trick”
邏輯部分
我是完全按照業(yè)務(wù)需求來設(shè)計,并沒有做抽象。其實(shí)這樣的一個下拉加載功能完全可以抽象出來。有興趣的讀者可以下去自己進(jìn)行封裝和抽象。
我們先把精力集中在邏輯處理上。
下面進(jìn)入我們最核心的邏輯部分,為了防止全局污染,我把它放入了一個立即執(zhí)行函數(shù)中:
(function() {
var fetching = false;
var page = 1;
var slideCache = [];
var itemMap = {};
var lastScrollY = window.pageYOffset;
var scrollY = window.pageYOffset;
var innerHeight;
var topViewPort;
var bottomViewPort;
function isVisible (id) {
// ...判斷元素是否在可見區(qū)域
}
function updateItemCache (node) {
// ....更新DOM緩存
}
function fetchContent () {
// ...ajax請求數(shù)據(jù)
}
function handleDefer () {
// ...懶加載實(shí)現(xiàn)
}
function handleScroll (e, force) {
// ...滾動處理程序
}
window.setTimeout(handleScroll, 100);
fetchContent();
}());
我認(rèn)為好的編程習(xí)慣是在程序開頭部分便聲明所有的變量,防止“變量提升”帶來的潛在困擾,并且也有利于程序的整體把控。
我們來看一下變量設(shè)置:
// 加載中狀態(tài)鎖
1)var fetching = false;
// 用于加載時發(fā)送請求參數(shù),表示第幾屏內(nèi)容,初始為1,以后每請求一次,遞增1
2)var page = 1;
// 只緩存最新一次下拉數(shù)據(jù)生成的DOM節(jié)點(diǎn),即需要插入的dom緩存數(shù)組
3)var slideCache = [];
// 用于已經(jīng)生成的DOM節(jié)點(diǎn)儲存,存有item的offsetTop,offsetHeight
4) var slideMap = {};
// pageYOffset設(shè)置或返回當(dāng)前頁面相對于窗口顯示區(qū)左上角的Y位置。
5)var lastScrollY = window.pageYOffset; var scrollY = window.pageYOffset;
// 瀏覽器窗口的視口(viewport)高度
6)var innerHeight;
// isVisible的上下閾值邊界
7) var topViewPort;
8) var bottomViewPort;
關(guān)于DOM cache的變量詳細(xì)說明,在后文有提供。
同樣,我們有5個函數(shù)。在上面的代碼中,注釋已經(jīng)寫明白了每個方法的具體作用。接下來,我們逐個分析。
滾動處理程序handleScroll
它接受兩個變量,第二個是一個布爾值force,表示是否強(qiáng)制觸發(fā)滾動程序執(zhí)行。
核心思路是:如果時間間隔100毫秒內(nèi),沒有發(fā)生滾動,且并未強(qiáng)制觸發(fā),則do nothing,間隔100毫秒之后再次查詢,然后直接return。
其中,是否發(fā)生滾動由lastScrollY === window.scrollY來判斷。
在100毫秒之內(nèi)發(fā)生滾動或者強(qiáng)制觸發(fā)時,需要判斷是否滾動已接近頁面底部。如果是,則拉取數(shù)據(jù),調(diào)用fetchContent方法,并調(diào)用懶加載方法handleDefer。
并且在這個處理程序中,我們計算出來了isVisible區(qū)域的上下閾值。我們使用600作為浮動區(qū)間,這么做的目的是在一定范圍內(nèi)提前加載圖片,節(jié)省用戶等待時間。當(dāng)然,如果我們進(jìn)行抽象時,可以把這個值進(jìn)行參數(shù)化。
function handleScroll (e, force) {
// 如果時間間隔內(nèi),沒有發(fā)生滾動,且并未強(qiáng)制觸發(fā)加載,則do nothing,再次間隔100毫秒之后查詢
if (!force && lastScrollY === window.scrollY) {
window.setTimeout(handleScroll, 100);
return;
}
else {
// 更新文檔滾動位置
lastScrollY = window.scrollY;
}
scrollY = window.scrollY;
// 瀏覽器窗口的視口(viewport)高度賦值
innerHeight = window.innerHeight;
// 計算isVisible上下閾值
topViewPort = scrollY - 1000;
bottomViewPort = scrollY + innerHeight + 600;
// 判斷是否需要加載
// document.body.offsetHeight;返回當(dāng)前網(wǎng)頁高度
if (window.scrollY + innerHeight + 200 > document.body.offsetHeight) {
fetchContent();
}
// 實(shí)現(xiàn)懶加載
handleDefer();
window.setTimeout(handleScroll, 100);
}
拉取數(shù)據(jù)
這里我用到了自己封裝的ajax接口方法,它基于zepto的ajax方法,只不過又手動采用了promise包裝一層。實(shí)現(xiàn)比較簡單,當(dāng)然有興趣可以找我要一下代碼,這里不再詳細(xì)說了。
我們使用前端模版進(jìn)行HTML渲染,同時調(diào)用updateItemCache,將此次數(shù)據(jù)拉取生成的DOM節(jié)點(diǎn)緩存。之后手動觸發(fā)handleScroll,更新文檔滾動位置和懶加載處理。
function fetchContent () {
// 設(shè)置加載狀態(tài)鎖
if (fetching) {
return;
}
else {
fetching = true;
}
ajax({
url: (!location.pathname.indexOf('/m/') ? '/m' : '')
+ '/list/asyn?page=' + page + (+new Date),
timeout: 300000,
dataType: 'json'
}).then(function (data) {
if (data.errno) {
return;
}
console.time('render');
var dataList = data.data.list;
var len = dataList.length;
var ulContainer = document.getElementById('expList');
var str = '';
var frag = document.createElement('div');
var tpl = __inline('content.tmpl');
for (var i = 0; i < len; i++) {
str = tpl({dataList: dataList});
}
frag.innerHTML = str;
ulContainer.appendChild(frag);
// 更新緩存
updateItemCache(frag);
// 已經(jīng)拉去完畢,設(shè)置標(biāo)識為true
fetching = false;
// 強(qiáng)制觸發(fā)
handleScroll(null, true);
page++;
console.timeEnd('render');
}, function (xhr, type) {
console.log('Refresh:Ajax Error!');
});
}
緩存對象
之前參數(shù)里提到過,一共有兩個用于緩存的對象/數(shù)組:
1)slideCache:緩存最近一次加載過的數(shù)據(jù)生成的DOM內(nèi)容,緩存方式為數(shù)組儲存:
slideCache = [
{
id: "s-97r45",
img: img DOM節(jié)點(diǎn),
node: 父容器DOM node,類似<div id="s-<#=v.eid#>" class="slide"></div>,
src: 圖片資源地址
},
...
]
slideCache由updateItemCache函數(shù)更新,主要用于懶加載時的賦值src。這樣我們做到“只寫入DOM”原則,不需要再從DOM讀取。
2)slideMap:緩存DOM節(jié)點(diǎn)的高度和offsetTop,以DOM節(jié)點(diǎn)的id為索引。存儲方式:
slideMap = {
s-97r45: {
node: DOM node,類似<div id="s-<#=v.eid#>" class="slide"></div>,
offTop: 300,
offsetHeight: 90
}
}
slideMap根據(jù)isVisible方法的參數(shù)進(jìn)行更新和讀取。使得我們在判斷是否isVisible時,大量減少讀取DOM的操作。
懶加載程序
在上面的滾動處理程序中,我們調(diào)用了handleDefer函數(shù)。我們看一下這個函數(shù)的實(shí)現(xiàn):
function handleDefer () {
// 時間記錄
console.time('defer');
// 獲取dom緩存
var list = slideCache;
// 對于遍歷list里的每一項(xiàng),都使用一個變量,而不是在循環(huán)內(nèi)部聲明。節(jié)省內(nèi)存,把性能高效,做到極致。
var thisImg;
for (var i = 0, len = list.length; i < len; i++) {
thisImg = list[i].img; // 這里我們都是從內(nèi)存中讀取,而不用讀取DOM節(jié)點(diǎn)
var deferSrc = list[i].src; // 這里我們都是從內(nèi)存中讀取,而不用讀取DOM節(jié)點(diǎn)
// 判斷元素是否可見
if (isVisible(list[i].id)) {
// 這個函數(shù)是圖片onload邏輯
var handler = function () {
var node = thisImg;
var src = deferSrc;
// 創(chuàng)建一個閉包
return function () {
node.src = src;
node.style.opacity = 1;
}
}
var img = new Image();
img.onload = handler();
img.src = list[i].src;
}
}
console.timeEnd('defer');
}
主要思路就是對DOM緩存中的每一項(xiàng)進(jìn)行循環(huán)遍歷。在循環(huán)中,判斷每一項(xiàng)是否已經(jīng)進(jìn)入isVisible區(qū)域。如果進(jìn)入isVisible區(qū)域,則對當(dāng)前項(xiàng)進(jìn)行真實(shí)src賦值,并設(shè)置opacity為1。
更新拉取數(shù)據(jù)生成的DOM緩存
針對每一個slide類,我們緩存對應(yīng)DOM節(jié)、id、子元素img DOM節(jié)點(diǎn):
function updateItemCache (node) {
var list = node.querySelectorAll('.slide');
var len = list.length;
slideCache = [];
var obj;
for (var i=0; i < len; i++) {
obj = {
node: list[i],
id: list[i].getAttribute('id'),
img: list[i].querySelector('.img')
}
obj.src = obj.img.getAttribute('data-src');
slideCache.push(obj);
};
}
是否在isVisible區(qū)域判斷
該函數(shù)接受相應(yīng)DOM id,并進(jìn)行判斷。
如果判斷條件晦澀難懂的話,你一定要手動畫畫圖理解一下。如果你就是懶得畫圖,那么也沒關(guān)系,我?guī)湍惝嫼昧?,只是丑一些。?!?/p>
function isVisible (id) {
var offTop;
var offsetHeight;
var data;
var node;
// 判斷此元素是否已經(jīng)懶加載正確渲染,分為在屏幕之上(已經(jīng)懶加載完畢)和屏幕外,已經(jīng)添加到dom中,但是還未請求圖片(懶加載之前)
if (itemMap[id]) {
// 直接獲取offTop,offsetHeight值
offTop = itemMap[id].offTop;
offsetHeight = itemMap[id].offsetHeight;
}
else {
// 設(shè)置該節(jié)點(diǎn),并且設(shè)置節(jié)點(diǎn)屬性:node,offTop,offsetHeight
node = document.getElementById(id);
// offsetHeight是自身元素的高度
offsetHeight = parseInt(node.offsetHeight);
// 元素的上外緣距離最近采用定位父元素內(nèi)壁的距離
offTop = parseInt(node.offsetTop);
}
if (offTop + offsetHeight > topViewPort && offTop < bottomViewPort) {
return true;
}
else {
return false;
}
}

性能收益
如上代碼,我們主要進(jìn)行了兩方面的性能考量:
1)延遲加載時間
2)渲染DOM時間
整體收益如下:
優(yōu)化前延遲平均值:49.2ms 中間值:43ms;
優(yōu)化后延遲平均值:17.1ms 中間值:11ms;
優(yōu)化前渲染平均值:2129.6ms 中間值:2153.5ms;
優(yōu)化后渲染平均值:120.5ms 中間值:86ms;
繼續(xù)思考
做完這些,其實(shí)也遠(yuǎn)遠(yuǎn)沒有達(dá)到所謂的“極致化”性能體驗(yàn)。我們無非就做了各種DOM緩存、映射、懶加載。如果繼續(xù)分析edge case,我們還能做的更多,比如:DOM回收、墓碑和滾動錨定。這些其實(shí)很多都是借鑒客戶端開發(fā)理念,但是超前的谷歌開發(fā)者團(tuán)隊也都有了自己的實(shí)現(xiàn)。比如在去年7月份的
一篇文章:Complexities of an Infinite Scroller就都有所提及。這里從原理(非代碼)層面,也給大家做個介紹。
DOM回收
它的原理是,對于需要產(chǎn)生的大量DOM節(jié)點(diǎn)(比如我們下拉加載的信息內(nèi)容)不是主動用createElement的方式創(chuàng)建,而是回收利用那些已經(jīng)移出視窗,暫時不會被需要的DOM節(jié)點(diǎn)。如圖:

雖然DOM節(jié)點(diǎn)本身并非耗能大戶,但是也不是一點(diǎn)都不消耗性能,每一個節(jié)點(diǎn)都會增加一些額外的內(nèi)存、布局、樣式和繪制。同樣需要注意的一點(diǎn)是,在一個較大的DOM中每一次重新布局或重新應(yīng)用樣式(在節(jié)點(diǎn)上增加或刪除樣式所觸發(fā)的過程)的系統(tǒng)開銷都會比較昂貴。所以進(jìn)行DOM回收意味著我們會保持DOM節(jié)點(diǎn)在一個比較低的數(shù)量上,進(jìn)而加快上面提到的這些處理過程。
據(jù)我觀察,在真正產(chǎn)品線上使用這項(xiàng)技術(shù)的還比較少??赡苁且?yàn)閷?shí)現(xiàn)復(fù)雜度和收益比并不很高。但是,淘寶移動端檢索頁面實(shí)現(xiàn)了類似的思想。如下圖,

每加載一次數(shù)據(jù),就生成“.page-container .J-PageContainer_頁數(shù)”的div,在滾動多屏之后,早已移除視窗的div的子節(jié)點(diǎn)進(jìn)行了remove(),并且為了保證滾動條的正確比例和防止高度塌陷,顯示聲明了2956px的高度。
墓碑(Tombstones)
如之前所說,如果網(wǎng)絡(luò)延遲較大,用戶又飛快地滾動,很容易就把我們渲染的DOM節(jié)點(diǎn)都甩在千里之外。這樣就會出現(xiàn)極差的用戶體驗(yàn)。針對這種情況,我們就需要一個墓碑條目占位在對應(yīng)位置。等到數(shù)據(jù)取到之后,再代替墓碑。墓碑也可以有一個獨(dú)立的DOM元素池。并且也可以設(shè)計出一些漂亮的過渡。這種技術(shù)在國外的一些“引領(lǐng)技術(shù)潮流”的網(wǎng)站上,早已經(jīng)有了應(yīng)有。比如下圖取自Facebook:

我在“簡書”APP客戶端上,也見過類似的方案。當(dāng)然,人家是native...

滾動錨定
滾動錨定的觸發(fā)時機(jī)有兩個:一個是墓碑被替換時,另一個是窗口大小發(fā)生改變時(在設(shè)備發(fā)生翻轉(zhuǎn)時也會發(fā)生)。這兩種情況,都需要調(diào)整對應(yīng)的滾動位置。
總結(jié)
當(dāng)你想提供一個高性能的有良好用戶體驗(yàn)的功能時,可能技術(shù)上一個簡單的問題,就會演變成復(fù)雜問題的。這篇文章便是一個例證。
隨著 “Progressive Web Apps” 逐漸成為移動設(shè)備的一等公民(會嗎?),高性能的良好體驗(yàn)會變得越來越重要。
開發(fā)者也必須持續(xù)的研究使用一些模式來應(yīng)對性能約束。這些設(shè)計的基礎(chǔ)當(dāng)然都是成熟的技術(shù)為根本。
這篇文章參考了Flicker工程師,前YAHOO工程師Stephen Woods的《Building Touch Interfaces with HTML5》一書。以及王芃前輩對于《Complexities of an Infinite Scroller》一文的部分翻譯。