高性能渲染十萬級數(shù)據(jù)(時間分片)

背景:在實際工作中浸颓,我們很少會遇到一次性需要向頁面中插入大量數(shù)據(jù)的情況
我們有必要了解并清楚當(dāng)遇到大量數(shù)據(jù)時愤钾,如何才能在不卡主頁面的情況下渲染數(shù)據(jù)蛙讥,以及其中背后的原理薪丁。

對于一次性插入大量數(shù)據(jù)的情況遇西,一般有兩種做法:
時間分片: 使用定時器
虛擬列表
著重來介紹如何使用 時間分片的方式來渲染大量數(shù)據(jù),虛擬列表相關(guān)的內(nèi)容严嗜,日后會持續(xù)整理粱檀。

最粗暴的做法(一次性渲染)

<ul id="container"></ul>
    <script>
        // 記錄任務(wù)開始時間
         let now = Date.now();
         // 插入十萬條數(shù)據(jù)
         const total = 100000;
         // 獲取容器
         let ul = document.getElementById('container');
         // 將數(shù)據(jù)插入容器
         for (let i = 0; i < total; i++) {
             let li = document.createElement('li');
             li.innerText = ~~(Math.random() * total)
             ul.appendChild(li)
         }
         console.log('js運行時間:', Date.now() - now);
         setTimeout(() => {
             console.log('總運行時間:', Date.now() - now);
         }, 0)
         // print: JS運行時間: 187
         // print: 總運行時間: 2844  
    </script>

我們對十萬條記錄進(jìn)行循環(huán)操作,JS的運行時間為 187ms漫玄,還是蠻快的茄蚯,但是最終渲染完成后的總時間是 2844ms。
簡單說明一下睦优,為何兩次 console.log的結(jié)果時間差異巨大渗常,并且是如何簡單來統(tǒng)計 JS運行時間和 總渲染時間:

  • 在 JS 的 EventLoop中,當(dāng)JS引擎所管理的執(zhí)行棧中的事件以及所有微任務(wù)事件全部執(zhí)行完后汗盘,才會觸發(fā)渲染線程對頁面進(jìn)行渲染
  • 第一個 console.log的觸發(fā)時間是在頁面進(jìn)行渲染之前皱碘,此時得到的間隔時間為JS運行所需要的時間
  • 第二個 console.log是放到 setTimeout 中的,它的觸發(fā)時間是在渲染完成隐孽,在下一次 EventLoop中執(zhí)行的
    依照兩次 console.log的結(jié)果癌椿,可以得出結(jié)論:
    對于大量數(shù)據(jù)渲染的時候,JS運算并不是性能的瓶頸菱阵,性能的瓶頸主要在于渲染階段如失, 頁面卡頓是由于同時渲染大量DOM所引起的

使用定時器

// 記錄任務(wù)開始時間
         let now = Date.now();
         // 獲取容器
         let ul = document.getElementById('container');
         // 插入十萬條數(shù)據(jù)
         const total = 100000;
         // 一次插入20條
        const once = 20;
        // 計算總頁數(shù)
        const page = total / once;
        // 每條記錄的索引
        let index = 0;
         // 循環(huán)加載數(shù)據(jù)
         function loop(curTotal, curIndex){
            if (curTotal <= 0){
                return false;
            }
            // 每頁多少條
            let pageCount = Math.min(curTotal, once);
            setTimeout(() => {
                for (var i = 0; i < pageCount; i++) {
                    let li = document.createElement('li');
                    li.innerText = curIndex + i + ":" + ~~(Math.random() * total)
                    ul.appendChild(li);
                }
                console.log('總運行時間:', Date.now() - now); // print: 2
                // loop(curTotal - pageCount, curIndex + pageCount)
            }, 0)
         }
         loop(total, index)
         console.log('js運行時間:', Date.now() - now); // print 0
    </script>

我們可以看到,頁面加載的時間已經(jīng)非乘土唬快了,每次刷新時可以很快的看到第一屏的所有數(shù)據(jù)掂之,但是當(dāng)我們快速滾動頁面的時候抗俄,會發(fā)現(xiàn)頁面出現(xiàn)閃屏或白屏的現(xiàn)象

為什么會出現(xiàn)閃屏現(xiàn)象呢
FPS表示的是每秒鐘畫面更新次數(shù)。
我們平時所看到的連續(xù)畫面都是由一幅幅靜止畫面組成的世舰,每幅畫面稱為一
FPS是描述 幀變化速度的物理
大多數(shù)電腦顯示器的刷新頻率是60Hz动雹,大概相當(dāng)于每秒鐘重繪60次, FPS為60frame/s跟压,為這個值的設(shè)定受屏幕分辨率胰蝠、屏幕尺寸和顯卡的影響。
因此,當(dāng)你對著電腦屏幕什么也不做的情況下茸塞,大多顯示器也會以每秒60次的頻率正在不斷的更新屏幕上的圖像躲庄。

為什么你感覺不到這個變化?

那是因為人的眼睛有視覺停留效應(yīng)钾虐,即前一副畫面留在大腦的印象還沒消失噪窘,緊接著后一副畫面就跟上來了, 這中間只間隔了16.7ms(1000/60≈16.7)效扫,所以會讓你誤以為屏幕上的圖像是靜止不動的倔监。
最平滑動畫的最佳循環(huán)間隔是1000ms/60,約等于16.6ms菌仁。
直觀感受浩习,不同幀率的體驗:

  • 幀率能夠達(dá)到 50 ~ 60 FPS 的動畫將會相當(dāng)流暢,讓人倍感舒適济丘;
  • 幀率在 30 ~ 50 FPS 之間的動畫谱秽,因各人敏感程度不同,舒適度因人而異闪盔;
  • 幀率在 30 FPS 以下的動畫弯院,讓人感覺到明顯的卡頓和不適感;
  • 幀率波動很大的動畫泪掀,亦會使人感覺到卡頓听绳。

簡單聊一下 setTimeout 和閃屏現(xiàn)象

setTimeout的執(zhí)行時間并不是確定的。在JS中异赫, setTimeout任務(wù)被放進(jìn)事件隊列中椅挣,只有主線程執(zhí)行完才會去檢查事件隊列中的任務(wù)是否需要執(zhí)行,因此 setTimeout的實際執(zhí)行時間可能會比其設(shè)定的時間晚一些塔拳。
刷新頻率受屏幕分辨率和屏幕尺寸的影響鼠证,因此不同設(shè)備的刷新頻率可能會不同,而 setTimeout只能設(shè)置一個固定時間間隔靠抑,這個時間不一定和屏幕的刷新時間相同量九。
以上兩種情況都會導(dǎo)致setTimeout的執(zhí)行步調(diào)和屏幕的刷新步調(diào)不一致

在 setTimeout中對dom進(jìn)行操作颂碧,必須要等到屏幕下次繪制時才能更新到屏幕上荠列,如果兩者步調(diào)不一致,就可能導(dǎo)致中間某一幀的操作被跨越過去载城,而直接更新下一幀的元素肌似,從而導(dǎo)致丟幀現(xiàn)象。

使用 requestAnimationFrame

與 setTimeout相比诉瓦, requestAnimationFrame最大的優(yōu)勢是由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時機川队。
如果屏幕刷新率是60Hz,那么回調(diào)函數(shù)就每16.7ms被執(zhí)行一次力细,如果刷新率是75Hz,那么這個時間間隔就變成了1000/75=13.3ms固额,換句話說就是眠蚂, requestAnimationFrame的步伐跟著系統(tǒng)的刷新步伐走。它能保證回調(diào)函數(shù)在屏幕每一次的刷新間隔中只被執(zhí)行一次对雪,這樣就不會引起丟幀現(xiàn)象河狐。

 <ul id="container"></ul>
    <script>
         // 記錄任務(wù)開始時間
         let now = Date.now();
         // 獲取容器
         let ul = document.getElementById('container');
         // 插入十萬條數(shù)據(jù)
         const total = 100000;
         // 一次插入20條
        const once = 20;
        // 計算總頁數(shù)
        const page = total / once;
        // 每條記錄的索引
        let index = 0;
         // 循環(huán)加載數(shù)據(jù)
         function loop(curTotal, curIndex){
            if (curTotal <= 0){
                return false;
            }
            // 每頁多少條
            let pageCount = Math.min(curTotal, once);
            console.log('js運行時間:', Date.now() - now);
            window.requestAnimationFrame(() => {
                for (var i = 0; i < pageCount; i++) {
                    let li = document.createElement('li');
                    li.innerText = curIndex + i + ":" + ~~(Math.random() * total)
                    ul.appendChild(li);
                }
                console.log('總運行時間:', Date.now() - now);
                loop(curTotal - pageCount, curIndex + pageCount)
            })
         }
         loop(total, index)
    </script>

使用 DocumentFragment

DocumentFragment,文檔片段接口瑟捣,表示一個沒有父級文件的最小文檔對象馋艺。它被作為一個輕量版的 Document使用,用于存儲已排好版的或尚未打理好格式的XML片段迈套。最大的區(qū)別是因為 DocumentFragment不是真實DOM樹的一部分捐祠,它的變化不會觸發(fā)DOM樹的(重新渲染) ,且不會導(dǎo)致性能等問題桑李。
可以使用 document.createDocumentFragment方法或者構(gòu)造函數(shù)來創(chuàng)建一個空的
從MDN的說明中踱蛀,我們得知 DocumentFragments是DOM節(jié)點,但并不是DOM樹的一部分贵白,可以認(rèn)為是存在內(nèi)存中的率拒,所以將子元素插入到文檔片段時不會引起頁面回流
當(dāng) append元素到 document中時禁荒,被 append進(jìn)去的元素的樣式表的計算是同步發(fā)生的猬膨,此時調(diào)用 getComputedStyle 可以得到樣式的計算值。而 append元素到 documentFragment 中時呛伴,是不會計算元素的樣式表勃痴,所以 documentFragment 性能更優(yōu)。當(dāng)然現(xiàn)在瀏覽器的優(yōu)化已經(jīng)做的很好了热康,
當(dāng) append元素到 document中后沛申,沒有訪問 getComputedStyle 之類的方法時,現(xiàn)代瀏覽器也可以把樣式表的計算推遲到腳本執(zhí)行之后姐军。

<ul id="container"></ul>
    <script>
         // 記錄任務(wù)開始時間
         let now = Date.now();
         // 獲取容器
         let ul = document.getElementById('container');
         // 插入十萬條數(shù)據(jù)
         const total = 100000;
         // 一次插入20條
        const once = 20;
        // 計算總頁數(shù)
        const page = total / once;
        // 每條記錄的索引
        let index = 0;
         // 循環(huán)加載數(shù)據(jù)
         function loop(curTotal, curIndex){
            if (curTotal <= 0){
                return false;
            }
            // 每頁多少條
            let pageCount = Math.min(curTotal, once);
            console.log('js運行時間:', Date.now() - now);
            window.requestAnimationFrame(() => {
                let fragment = document.createDocumentFragment();
                for (var i = 0; i < pageCount; i++) {
                    let li = document.createElement('li');
                    li.innerText = curIndex + i + ":" + ~~(Math.random() * total)
                    fragment.appendChild(li);
                }
                ul.appendChild(fragment);
                console.log('總運行時間:', Date.now() - now);
                loop(curTotal - pageCount, curIndex + pageCount)
            })
         }
         loop(total, index)
    </script>

最后

本文更多的是提供一個思路铁材,通過時間分片的方式來同時加載大量簡單DOM。對于復(fù)雜DOM的情況奕锌,一般會用到虛擬列表的方式來實現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衫贬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子歇攻,更是在濱河造成了極大的恐慌,老刑警劉巖梆造,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缴守,死亡現(xiàn)場離奇詭異葬毫,居然都是意外死亡,警方通過查閱死者的電腦和手機屡穗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門贴捡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人村砂,你說我怎么就攤上這事烂斋。” “怎么了础废?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵汛骂,是天一觀的道長。 經(jīng)常有香客問我评腺,道長帘瞭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任蒿讥,我火速辦了婚禮蝶念,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芋绸。我一直安慰自己媒殉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布摔敛。 她就那樣靜靜地躺著廷蓉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舷夺。 梳的紋絲不亂的頭發(fā)上苦酱,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音给猾,去河邊找鬼疫萤。 笑死,一個胖子當(dāng)著我的面吹牛敢伸,可吹牛的內(nèi)容都是我干的扯饶。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼池颈,長吁一口氣:“原來是場噩夢啊……” “哼尾序!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起躯砰,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤每币,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后琢歇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兰怠,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡梦鉴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了揭保。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肥橙。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秸侣,靈堂內(nèi)的尸體忽然破棺而出存筏,到底是詐尸還是另有隱情,我是刑警寧澤味榛,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布椭坚,位于F島的核電站,受9級特大地震影響励负,放射性物質(zhì)發(fā)生泄漏藕溅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一继榆、第九天 我趴在偏房一處隱蔽的房頂上張望巾表。 院中可真熱鬧,春花似錦略吨、人聲如沸集币。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鞠苟。三九已至,卻和暖如春秽之,著一層夾襖步出監(jiān)牢的瞬間当娱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工考榨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跨细,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓河质,卻偏偏與公主長得像冀惭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掀鹅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

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