設(shè)計無限滾動下拉加載蛤铜,實踐高性能頁面真諦

UX Planet論壇上有過這么一篇熱門文章: Infinite Scrolling Best Practices官研,它從UX角度分析了無限滾動加載的設(shè)計實踐。

無限滾動加載在互聯(lián)網(wǎng)上到處都有應用:
豆瓣首頁是一個觉既,F(xiàn)acebook的Timeline是一個惧盹,Tweeter的話題列表也是一個。當你向下滾動瞪讼,新的內(nèi)容就神奇的“無中生有”了钧椰。這是一個得到廣泛贊揚的用戶體驗。

無限滾動加載背后的技術(shù)挑戰(zhàn)其實比想象中要多不少符欠。尤其是要考慮頁面性能嫡霞,需要做到極致。
本文通過代碼實例希柿,來實現(xiàn)一個無限滾動加載效果诊沪。更重要的是,在實現(xiàn)過程中曾撤,對于頁面性能的分析和處理力圖做到最大化端姚,希望對讀者有所啟發(fā),同時也歡迎與我討論挤悉。

性能測量

在開啟我們的代碼之前渐裸,有必要先了解一下常用的性能測量手段:

1)使用window.performance

HTML5帶來的performance API功能強大。我們可以使用其performance.now()精確計算程序執(zhí)行時間。performance.now()與Date.now()不同的是昏鹃,返回了以微秒(百萬分之一秒)為單位的時間尚氛,更加精準。并且與 Date.now() 會受系統(tǒng)程序執(zhí)行阻塞的影響不同洞渤,performance.now() 的時間是以恒定速率遞增的阅嘶,不受系統(tǒng)時間的影響(系統(tǒng)時間可被人為或軟件調(diào)整)。
同時载迄,也可以使用performance.mark()標記各種時間戳(就像在地圖上打點)讯柔,保存為各種測量值(測量地圖上的點之間的距離),便可以批量地分析這些數(shù)據(jù)了宪巨。

2)使用console.time方法與console.timeEnd方法

其中console.time方法用于標記開始時間磷杏,console.timeEnd方法用于標記結(jié)束時間,并且將結(jié)束時間與開始時間之間經(jīng)過的毫秒數(shù)在控制臺中輸出捏卓。

3)使用專業(yè)的測量工具/平臺:jsPerf

這次實現(xiàn)中极祸,我們使用第二種方法,因為它已經(jīng)完全可以滿足我們的需求怠晴,且兼容性更加全面遥金。

整體思路和方案設(shè)計

我們要實現(xiàn)的頁面樣例如圖,

ye mian

它能夠做到無限下拉加載內(nèi)容蒜田。我把紅線標出的部分叫做一個block-item稿械,后續(xù)也都用這種命名。

1)關(guān)于設(shè)計方案冲粤,肯定第一個最基本美莫、最樸素的思想是下拉到底部之后發(fā)送ajax異步請求,成功之后的回調(diào)里進行頁面拼接梯捕。

2)但是觀察頁面布局厢呵,很明顯圖片較多,每一個block-item區(qū)塊都有一張配圖傀顾。當加載后的內(nèi)容插入到頁面中時襟铭,瀏覽器就開始獲取圖片。這意味著所有的圖像同時下載短曾,瀏覽器中的下載通道將被占滿寒砖。同時,由于內(nèi)容優(yōu)先于用戶瀏覽而加載嫉拐,所以可能被迫下載底部那些永遠也不會被用戶瀏覽到的圖像哩都。
所以,我們需要設(shè)計一個懶加載效果婉徘,使得頁面速度更快漠嵌,并且節(jié)省用戶的流量費用和延長電池壽命璃赡。

3)上一條提到的懶加載實現(xiàn)上,為了避免到真正的頁面底部時才進行加載和渲染献雅,而造成用戶較長時間等待。我們可以設(shè)置一個合理閾值塌计,在用戶滾動到頁面底部之前挺身,先進行提前加載。

4)另外锌仅,頁面滾動的事件肯定是需要監(jiān)聽的章钾。同時,頁面滾動問題也比較棘手热芹,后面將專為滾動進行分析贱傀。

5)DOM操作我們知道是及其緩慢而低效的,有興趣的同學可以研究一下jsPerf上一些經(jīng)典的benchmark伊脓,比如這篇府寒。關(guān)于造成這種緩慢的原因,社區(qū)上同樣有很多文章有過分析报腔,這里就不再深入株搔。但我想總結(jié)并補充的是:DOM操作,光是為了找一個節(jié)點纯蛾,就從本質(zhì)上比簡單的檢索內(nèi)存中的值要慢纤房。一些DOM操作還需要重新計算樣式來讀取或檢索一個值。更突出的問題在于:DOM操作是阻塞的翻诉,所以當有一個DOM操作在進行時炮姨,其他的什么都不能做,包括用戶與頁面的交互(除了滾動)碰煌。這是一個極度傷害用戶體驗的事實舒岸。

所以,在下面的效果實現(xiàn)中拄查,我采用了大量“不可思議”的DOM緩存吁津,甚至極端的緩存everything。當然堕扶,這樣做的收益也在最后部分有所展現(xiàn)碍脏。

滾動問題

滾動問題不難想象在于高頻率的觸發(fā)滾動事件處理上。具我親測稍算,在極端case下典尾,滾動及其卡頓。即使?jié)L動不卡頓糊探,你可以打開Chrome控制臺發(fā)現(xiàn)钾埂,幀速率也非常慢河闰。關(guān)于幀速率的問題,我們有著名的16.7毫秒理論褥紫。關(guān)于這個時間分析姜性,社區(qū)上也有不少文章闡述,這里不再展開髓考。

針對于此部念,有很多讀者會立刻想到“截流和防抖動函數(shù)”(Throttle和Debounce)。
簡單總結(jié)一下:

1)Throttle允許我們限制激活響應的數(shù)量氨菇。我們可以限制每秒回調(diào)的數(shù)量儡炼。反過來,也就是說在激活下一個回調(diào)之前要等待多少時間;

2)Debounce意味著當事件發(fā)生時查蓉,我們不會立即激活回調(diào)乌询。相反,我們等待一定的時間并檢查相同的事件是否再次觸發(fā)豌研。如果是妹田,我們重置定時器,并再次等待聂沙。如果在等待期間沒有發(fā)生相同的事件秆麸,我們就立即激活回調(diào)。

具體這里就不代碼實現(xiàn)了及汉。原理明白之后沮趣,應該不難寫出。

但是我這里想從移動端主要瀏覽器處理滾動的方式入手坷随,來思考這個問題:

1)在Android機器上房铭,用戶滾動屏幕時,滾動事件高頻率發(fā)生——在Galaxy-SIII手機上温眉,大約頻率是一秒一百次缸匪。這意味著,滾動處理函數(shù)也被調(diào)用了數(shù)百次类溢,而這些又都是成本較大的函數(shù)凌蔬。

2)在Safari瀏覽器上,我們遇到的問題恰恰是相反的:用戶每次滾動屏幕時闯冷,滾動事件只在滾動動畫停止時才觸發(fā)砂心。當用戶在iPhone上滾動屏幕時,不會運行更新界面的代碼(滾動停止時才會運行一次)蛇耀。

另外辩诞,我想也許會有讀者想到rAf(requestAnimationFrame),但是據(jù)我觀察纺涤,很多前端其實并不明白requestAnimationFrame技術(shù)的原理和解決的問題译暂。只是機械地把動畫性能抠忘、掉幀問題甩到這么一個名詞上。在真實項目中外永,也沒有親自實現(xiàn)過崎脉,更不要說考慮requestAnimationFrame的兼容性情況了。這里場景我并不會使用rAf伯顶,因為荧嵌。setTimeout的定時器值推薦最小使用16.7ms(原因請去社區(qū)上找答案,不再細講)砾淌,我們這里并不會超過這個限制,并且考慮兼容性谭网。關(guān)于這項技術(shù)的使用汪厨,如果有問題,歡迎留言討論愉择。

基于以上劫乱,我的解決方案是既不同于Throttle,也不同于Debounce锥涕,但是和這兩個思想衷戈,尤其是Throttle又比較類似:把滾動事件替換為一個帶有計時器的滾動處理程序,每100毫秒進行簡單檢查层坠,看這段時間內(nèi)用戶是否滾動過殖妇。如果沒有,則什么都不做破花;如果有谦趣,就進行處理。

用戶體驗優(yōu)化小竅門

在圖像加載完成時座每,使用淡入(fade in)效果出現(xiàn)前鹅。這在實際情況上會稍微慢一下,應該慢一個過渡執(zhí)行時間峭梳。但用戶體驗上感覺會更快舰绘。這是已經(jīng)被證實且普遍應用的小“trick”。但是據(jù)我感覺葱椭,它確實有效捂寿。我們的代碼實現(xiàn)也采用了這個小竅門。不過類似這種“社會心理學”范疇的東西挫以,顯然不是本文研究的重點者蠕。

總結(jié)一下

代碼上將會采用:超前閾值的懶加載+DOM Cache和圖片Cache+滾動throttle模擬+CSS fadeIn動畫。
具體功能封裝上和一些實現(xiàn)層面的東西掐松,請您繼續(xù)閱讀踱侣。

代碼實現(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)容的容器。
因為每次加載并append進入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ù)填充待榔,并添加進入頁面逞壁,構(gòu)成每個block-item。
這里需要注意觀察锐锣,有助于對后面邏輯的理解腌闯。頁面中一個block-item下div屬性存有該block-item的eid值,對應class叫做"slide"雕憔,子孫節(jié)點包含有一個image標簽姿骏,src初始賦值為1px的空白圖進行占位。真實圖片資源位置存儲在"data-src"中斤彼。
另外分瘦,請求返回的數(shù)據(jù)dataList可以理解為由9個對象構(gòu)成的數(shù)組,也就是說琉苇,每次請求加載9個block-item嘲玫。

樣式亮點

樣式方面不是這篇文章的重點,挑選最核心的一行來說明一下:

    .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屬性實現(xiàn)一個fade in效果。
對應我們上面所提到的那個“trick”

邏輯部分

我是完全按照業(yè)務需求來設(shè)計穷蛹,并沒有做抽象渗勘。其實這樣的一個下拉加載功能完全可以抽象出來。有興趣的讀者可以下去自己進行封裝和抽象俩莽。
我們先把精力集中在邏輯處理上旺坠。
下面進入我們最核心的邏輯部分,為了防止全局污染扮超,我把它放入了一個立即執(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 () {
            // ...懶加載實現(xiàn)
        }

        function handleScroll (e, force) {
            // ...滾動處理程序
        } 
        
        window.setTimeout(handleScroll, 100);
        fetchContent();
    }());

我認為好的編程習慣是在程序開頭部分便聲明所有的變量取刃,防止“變量提升”帶來的潛在困擾,并且也有利于程序的整體把控出刷。
我們來看一下變量設(shè)置:

    // 加載中狀態(tài)鎖
    1)var fetching = false;
    // 用于加載時發(fā)送請求參數(shù)璧疗,表示第幾屏內(nèi)容,初始為1馁龟,以后每請求一次崩侠,遞增1
    2)var page = 1; 
    // 只緩存最新一次下拉數(shù)據(jù)生成的DOM節(jié)點,即需要插入的dom緩存數(shù)組
    3)var slideCache = []; 
    // 用于已經(jīng)生成的DOM節(jié)點儲存坷檩,存有item的offsetTop却音,offsetHeight
    4) var slideMap = {}; 
    // pageYOffset設(shè)置或返回當前頁面相對于窗口顯示區(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的變量詳細說明,在后文有提供系瓢。

同樣阿纤,我們有5個函數(shù)。在上面的代碼中夷陋,注釋已經(jīng)寫明白了每個方法的具體作用欠拾。接下來,我們逐個分析骗绕。

滾動處理程序handleScroll

它接受兩個變量藐窄,第二個是一個布爾值force,表示是否強制觸發(fā)滾動程序執(zhí)行酬土。

核心思路是:如果時間間隔100毫秒內(nèi)枷邪,沒有發(fā)生滾動,且并未強制觸發(fā)诺凡,則do nothing,間隔100毫秒之后再次查詢践惑,然后直接return腹泌。
其中,是否發(fā)生滾動由lastScrollY === window.scrollY來判斷尔觉。
在100毫秒之內(nèi)發(fā)生滾動或者強制觸發(fā)時凉袱,需要判斷是否滾動已接近頁面底部。如果是侦铜,則拉取數(shù)據(jù)专甩,調(diào)用fetchContent方法,并調(diào)用懶加載方法handleDefer钉稍。
并且在這個處理程序中涤躲,我們計算出來了isVisible區(qū)域的上下閾值。我們使用600作為浮動區(qū)間贡未,這么做的目的是在一定范圍內(nèi)提前加載圖片种樱,節(jié)省用戶等待時間。當然俊卤,如果我們進行抽象時嫩挤,可以把這個值進行參數(shù)化。

    function handleScroll (e, force) {
        // 如果時間間隔內(nèi)消恍,沒有發(fā)生滾動岂昭,且并未強制觸發(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;返回當前網(wǎng)頁高度 
        if (window.scrollY + innerHeight + 200 > document.body.offsetHeight) {
            fetchContent();
        }
        // 實現(xiàn)懶加載
        handleDefer();
        window.setTimeout(handleScroll, 100);
    } 

拉取數(shù)據(jù)

這里我用到了自己封裝的ajax接口方法约啊,它基于zepto的ajax方法邑遏,只不過又手動采用了promise包裝一層。實現(xiàn)比較簡單棍苹,當然有興趣可以找我要一下代碼无宿,這里不再詳細說了。
我們使用前端模版進行HTML渲染枢里,同時調(diào)用updateItemCache孽鸡,將此次數(shù)據(jù)拉取生成的DOM節(jié)點緩存。之后手動觸發(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è)置標識為true
            fetching = false;
            // 強制觸發(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é)點,
            node: 父容器DOM node,類似<div id="s-<#=v.eid#>" class="slide"></div>,
            src: 圖片資源地址
        },
        ...
    ]

slideCache由updateItemCache函數(shù)更新,主要用于懶加載時的賦值src灵奖。這樣我們做到“只寫入DOM”原則嚼沿,不需要再從DOM讀取。

2)slideMap:緩存DOM節(jié)點的高度和offsetTop瓷患,以DOM節(jié)點的id為索引骡尽。存儲方式:

    slideMap = {
        s-97r45: {
            node: DOM node,類似<div id="s-<#=v.eid#>" class="slide"></div>,
            offTop: 300,
            offsetHeight: 90
        }
    }

slideMap根據(jù)isVisible方法的參數(shù)進行更新和讀取。使得我們在判斷是否isVisible時擅编,大量減少讀取DOM的操作攀细。

懶加載程序

在上面的滾動處理程序中,我們調(diào)用了handleDefer函數(shù)爱态。我們看一下這個函數(shù)的實現(xiàn):

    function handleDefer () {
        // 時間記錄
        console.time('defer');

        // 獲取dom緩存
        var list = slideCache;
        // 對于遍歷list里的每一項谭贪,都使用一個變量,而不是在循環(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é)點
            var deferSrc = list[i].src; // 這里我們都是從內(nèi)存中讀取,而不用讀取DOM節(jié)點
            // 判斷元素是否可見
            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緩存中的每一項進行循環(huán)遍歷痘煤。在循環(huán)中凑阶,判斷每一項是否已經(jīng)進入isVisible區(qū)域。如果進入isVisible區(qū)域衷快,則對當前項進行真實src賦值宙橱,并設(shè)置opacity為1。

更新拉取數(shù)據(jù)生成的DOM緩存

針對每一個slide類,我們緩存對應DOM節(jié)师郑、id环葵、子元素img DOM節(jié)點:

    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ù)接受相應DOM id,并進行判斷宝冕。
如果判斷條件晦澀難懂的話张遭,你一定要手動畫畫圖理解一下。如果你就是懶得畫圖地梨,那么也沒關(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é)點腰素,并且設(shè)置節(jié)點屬性: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;
        }
    }
手繪圖示

性能收益

如上代碼弓千,我們主要進行了兩方面的性能考量:

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ù)思考

做完這些征唬,其實也遠遠沒有達到所謂的“極致化”性能體驗。我們無非就做了各種DOM緩存茁彭、映射总寒、懶加載。如果繼續(xù)分析edge case理肺,我們還能做的更多摄闸,比如:DOM回收、墓碑和滾動錨定妹萨。這些其實很多都是借鑒客戶端開發(fā)理念年枕,但是超前的谷歌開發(fā)者團隊也都有了自己的實現(xiàn)。比如在去年7月份的
一篇文章:Complexities of an Infinite Scroller就都有所提及乎完。這里從原理(非代碼)層面熏兄,也給大家做個介紹。

DOM回收

它的原理是,對于需要產(chǎn)生的大量DOM節(jié)點(比如我們下拉加載的信息內(nèi)容)不是主動用createElement的方式創(chuàng)建摩桶,而是回收利用那些已經(jīng)移出視窗桥状,暫時不會被需要的DOM節(jié)點。如圖:

動圖(盜圖)

雖然DOM節(jié)點本身并非耗能大戶硝清,但是也不是一點都不消耗性能辅斟,每一個節(jié)點都會增加一些額外的內(nèi)存、布局芦拿、樣式和繪制士飒。同樣需要注意的一點是,在一個較大的DOM中每一次重新布局或重新應用樣式(在節(jié)點上增加或刪除樣式所觸發(fā)的過程)的系統(tǒng)開銷都會比較昂貴防嗡。所以進行DOM回收意味著我們會保持DOM節(jié)點在一個比較低的數(shù)量上变汪,進而加快上面提到的這些處理過程。

據(jù)我觀察蚁趁,在真正產(chǎn)品線上使用這項技術(shù)的還比較少裙盾。可能是因為實現(xiàn)復雜度和收益比并不很高他嫡。但是番官,淘寶移動端檢索頁面實現(xiàn)了類似的思想。如下圖钢属,

淘寶做法

每加載一次數(shù)據(jù)徘熔,就生成“.page-container .J-PageContainer_頁數(shù)”的div,在滾動多屏之后淆党,早已移除視窗的div的子節(jié)點進行了remove()酷师,并且為了保證滾動條的正確比例和防止高度塌陷,顯示聲明了2956px的高度染乌。

墓碑(Tombstones)

如之前所說山孔,如果網(wǎng)絡延遲較大,用戶又飛快地滾動荷憋,很容易就把我們渲染的DOM節(jié)點都甩在千里之外台颠。這樣就會出現(xiàn)極差的用戶體驗。針對這種情況勒庄,我們就需要一個墓碑條目占位在對應位置串前。等到數(shù)據(jù)取到之后,再代替墓碑实蔽。墓碑也可以有一個獨立的DOM元素池荡碾。并且也可以設(shè)計出一些漂亮的過渡。這種技術(shù)在國外的一些“引領(lǐng)技術(shù)潮流”的網(wǎng)站上局装,早已經(jīng)有了應有玩荠。比如下圖取自Facebook:

Facebook墓碑

我在“簡書”APP客戶端上漆腌,也見過類似的方案。當然阶冈,人家是native...

簡書客戶端

滾動錨定

滾動錨定的觸發(fā)時機有兩個:一個是墓碑被替換時闷尿,另一個是窗口大小發(fā)生改變時(在設(shè)備發(fā)生翻轉(zhuǎn)時也會發(fā)生)。這兩種情況女坑,都需要調(diào)整對應的滾動位置填具。

總結(jié)

當你想提供一個高性能的有良好用戶體驗的功能時,可能技術(shù)上一個簡單的問題匆骗,就會演變成復雜問題的劳景。這篇文章便是一個例證。
隨著 “Progressive Web Apps” 逐漸成為移動設(shè)備的一等公民(會嗎碉就?)盟广,高性能的良好體驗會變得越來越重要。
開發(fā)者也必須持續(xù)的研究使用一些模式來應對性能約束瓮钥。這些設(shè)計的基礎(chǔ)當然都是成熟的技術(shù)為根本筋量。

這篇文章參考了Flicker工程師,前YAHOO工程師Stephen Woods的《Building Touch Interfaces with HTML5》一書碉熄。以及王芃前輩對于《Complexities of an Infinite Scroller》一文的部分翻譯桨武。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锈津,隨后出現(xiàn)的幾起案子呀酸,更是在濱河造成了極大的恐慌,老刑警劉巖琼梆,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件性誉,死亡現(xiàn)場離奇詭異,居然都是意外死亡茎杂,警方通過查閱死者的電腦和手機错览,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛉顽,“玉大人蝗砾,你說我怎么就攤上這事先较。” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵竖席,是天一觀的道長灯变。 經(jīng)常有香客問我,道長菜循,這世上最難降的妖魔是什么翘地? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上衙耕,老公的妹妹穿的比我還像新娘昧穿。我一直安慰自己,他們只是感情好橙喘,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布时鸵。 她就那樣靜靜地躺著,像睡著了一般厅瞎。 火紅的嫁衣襯著肌膚如雪饰潜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天和簸,我揣著相機與錄音彭雾,去河邊找鬼。 笑死锁保,一個胖子當著我的面吹牛薯酝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播身诺,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蜜托,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霉赡?” 一聲冷哼從身側(cè)響起橄务,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穴亏,沒想到半個月后蜂挪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嗓化,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年棠涮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刺覆。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡严肪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谦屑,到底是詐尸還是另有隱情驳糯,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布氢橙,位于F島的核電站酝枢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悍手。R本人自食惡果不足惜帘睦,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一袍患、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竣付,春花似錦诡延、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赤兴,卻和暖如春妖滔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桶良。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工座舍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陨帆。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓曲秉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疲牵。 傳聞我的和親對象是個殘疾皇子承二,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評論 25 707
  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標簽默認的外補...
    _Yfling閱讀 13,728評論 1 92
  • 今天我們學校開學了纲爸,我太多事亥鸠,回家給孩子讀了兩本書就睡了。 ①熊多多系列——我去市場了识啦。 ②鼠小弟系列——鼠小弟和...
    Lynn_1f06閱讀 109評論 0 1
  • 思念一個人负蚊,是什么滋味? 午夜很累很累颓哮,卻無法睡眠家妆,腦子里浮現(xiàn)出來一個身影,想你冕茅,想你伤极,就這樣折磨著自己
    真的自我閱讀 188評論 0 0
  • 我叫她‘頭大’,她叫我‘臉長’姨伤。 她說她不喜歡我這樣叫她哨坪,我說我也不喜歡你這樣叫我。她說‘可是你的臉就是很長啊’姜挺,...
    河馬_閱讀 596評論 0 1