「前端進階」高性能渲染十萬條數據(時間分片)

你知道的越多,你不知道的越多
點贊再看,手留餘香,與有榮焉javascript

前言

在實際工做中,咱們不多會遇到一次性須要向頁面中插入大量數據的狀況,可是爲了豐富咱們的知識體系,咱們有必要了解並清楚當遇到大量數據時,如何才能在不卡主頁面的狀況下渲染數據,以及其中背後的原理。html

對於一次性插入大量數據的狀況,通常有兩種作法:前端

  1. 時間分片
  2. 虛擬列表

本文做爲開篇,着重來介紹如何使用時間分片的方式來渲染大量數據,虛擬列表相關的內容,參見「前端進階」高性能渲染十萬條數據(虛擬列表)java

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

咱們先來看看最粗暴的作法,一次性將大量數據插入到頁面中:數組

<ul id="container"></ul>
複製代碼
// 記錄任務開始時間
let now = Date.now();
// 插入十萬條數據
const total = 100000;
// 獲取容器
let ul = document.getElementById('container');
// 將數據插入容器中
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
複製代碼

咱們對十萬條記錄進行循環操做,JS的運行時間爲187ms,仍是蠻快的,可是最終渲染完成後的總時間確是2844ms瀏覽器

簡單說明一下,爲什麼兩次console.log的結果時間差別巨大,而且是如何簡單來統計JS運行時間總渲染時間markdown

  • 在 JS 的Event Loop中,當JS引擎所管理的執行棧中的事件以及全部微任務事件所有執行完後,纔會觸發渲染線程對頁面進行渲染
  • 第一個console.log的觸發時間是在頁面進行渲染以前,此時獲得的間隔時間爲JS運行所須要的時間
  • 第二個console.log是放到 setTimeout 中的,它的觸發時間是在渲染完成,在下一次Event Loop中執行的

關於Event Loop的詳細內容請參見這篇文章-->多線程

依照兩次console.log的結果,能夠得出結論:app

對於大量數據渲染的時候,JS運算並非性能的瓶頸,性能的瓶頸主要在於渲染階段dom

使用定時器

從上面的例子,咱們已經知道,頁面的卡頓是因爲同時渲染大量DOM所引發的,因此咱們考慮將渲染過程分批進行

在這裏,咱們使用setTimeout來實現分批渲染

<ul id="container"></ul>
複製代碼
//須要插入的容器
let ul = document.getElementById('container');
// 插入十萬條數據
let total = 100000;
// 一次插入 20 條
let once = 20;
//總頁數
let page = total/once
//每條記錄的索引
let index = 0;
//循環加載數據
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每頁多少條
    let pageCount = Math.min(curTotal , once);
    setTimeout(()=>{
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    },0)
}
loop(total,index);
複製代碼

用一個gif圖來看一下效果

咱們能夠看到,頁面加載的時間已經很是快了,每次刷新時能夠很快的看到第一屏的全部數據,可是當咱們快速滾動頁面的時候,會發現頁面出現閃屏或白屏的現象

爲何會出現閃屏現象呢

首先,理清一些概念。FPS表示的是每秒鐘畫面更新次數。咱們平時所看到的連續畫面都是由一幅幅靜止畫面組成的,每幅畫面稱爲一FPS是描述變化速度的物理量。

大多數電腦顯示器的刷新頻率是60Hz,大概至關於每秒鐘重繪60次,FPS爲60frame/s,爲這個值的設定受屏幕分辨率、屏幕尺寸和顯卡的影響。

所以,當你對着電腦屏幕什麼也不作的狀況下,大多顯示器也會以每秒60次的頻率正在不斷的更新屏幕上的圖像。

爲何你感受不到這個變化?

那是由於人的眼睛有視覺停留效應,即前一副畫面留在大腦的印象還沒消失,緊接着後一副畫面就跟上來了, 這中間只間隔了16.7ms(1000/60≈16.7),因此會讓你誤覺得屏幕上的圖像是靜止不動的。

而屏幕給你的這種感受是對的,試想一下,若是刷新頻率變成1次/秒,屏幕上的圖像就會出現嚴重的閃爍, 這樣就很容易引發眼睛疲勞、痠痛和頭暈目眩等症狀。

大多數瀏覽器都會對重繪操做加以限制,不超過顯示器的重繪頻率,由於即便超過那個頻率用戶體驗也不會有提高。 所以,最平滑動畫的最佳循環間隔是1000ms/60,約等於16.6ms。

直觀感覺,不一樣幀率的體驗:

  • 幀率可以達到 50 ~ 60 FPS 的動畫將會至關流暢,讓人倍感溫馨;
  • 幀率在 30 ~ 50 FPS 之間的動畫,因各人敏感程度不一樣,溫馨度因人而異;
  • 幀率在 30 FPS 如下的動畫,讓人感受到明顯的卡頓和不適感;
  • 幀率波動很大的動畫,亦會令人感受到卡頓。

簡單聊一下 setTimeout 和閃屏現象

  • setTimeout的執行時間並非肯定的。在JS中,setTimeout任務被放進事件隊列中,只有主線程執行完纔會去檢查事件隊列中的任務是否須要執行,所以setTimeout的實際執行時間可能會比其設定的時間晚一些。
  • 刷新頻率受屏幕分辨率和屏幕尺寸的影響,所以不一樣設備的刷新頻率可能會不一樣,而setTimeout只能設置一個固定時間間隔,這個時間不必定和屏幕的刷新時間相同。

以上兩種狀況都會致使setTimeout的執行步調和屏幕的刷新步調不一致。

setTimeout中對dom進行操做,必需要等到屏幕下次繪製時才能更新到屏幕上,若是二者步調不一致,就可能致使中間某一幀的操做被跨越過去,而直接更新下一幀的元素,從而致使丟幀現象。

使用 requestAnimationFrame

setTimeout相比,requestAnimationFrame最大的優點是由系統來決定回調函數的執行時機。

若是屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,若是刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟着系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象。

咱們使用requestAnimationFrame來進行分批渲染:

<ul id="container"></ul>
複製代碼
//須要插入的容器
let ul = document.getElementById('container');
// 插入十萬條數據
let total = 100000;
// 一次插入 20 條
let once = 20;
//總頁數
let page = total/once
//每條記錄的索引
let index = 0;
//循環加載數據
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每頁多少條
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);
複製代碼

看下效果

咱們能夠看到,頁面加載的速度很快,而且滾動的時候,也很流暢沒有出現閃爍丟幀的現象。

這就結束了麼,還能夠再優化麼?

固然~~

使用 DocumentFragment

先解釋一下什麼是 DocumentFragment ,文獻引用自MDN

DocumentFragment,文檔片斷接口,表示一個沒有父級文件的最小文檔對象。它被做爲一個輕量版的Document使用,用於存儲已排好版的或還沒有打理好格式的XML片斷。最大的區別是由於DocumentFragment不是真實DOM樹的一部分,它的變化不會觸發DOM樹的(從新渲染) ,且不會致使性能等問題。
可使用document.createDocumentFragment方法或者構造函數來建立一個空的DocumentFragment

從MDN的說明中,咱們得知DocumentFragments是DOM節點,但並非DOM樹的一部分,能夠認爲是存在內存中的,因此將子元素插入到文檔片斷時不會引發頁面迴流。

append元素到document中時,被append進去的元素的樣式表的計算是同步發生的,此時調用 getComputedStyle 能夠獲得樣式的計算值。 而append元素到documentFragment 中時,是不會計算元素的樣式表,因此documentFragment 性能更優。固然如今瀏覽器的優化已經作的很好了, 當append元素到document中後,沒有訪問 getComputedStyle 之類的方法時,現代瀏覽器也能夠把樣式表的計算推遲到腳本執行以後。

最後修改代碼以下:

<ul id="container"></ul>
複製代碼
//須要插入的容器
let ul = document.getElementById('container');
// 插入十萬條數據
let total = 100000;
// 一次插入 20 條
let once = 20;
//總頁數
let page = total/once
//每條記錄的索引
let index = 0;
//循環加載數據
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每頁多少條
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        let fragment = document.createDocumentFragment();
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            fragment.appendChild(li)
        }
        ul.appendChild(fragment)
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);
複製代碼

最後

本文更多的是提供一個思路,經過時間分片的方式來同時加載大量簡單DOM。對於複雜DOM的狀況,通常會用到虛擬列表的方式來實現,關於這一問題,會持續整理,敬請期待。

系列文章推薦

參考

相關文章
相關標籤/搜索