頁面須要渲染10萬條數據,應該怎麼實現?

關鍵點:不卡頓,交互流暢

1、最傳統、最簡單粗暴的方式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>如何渲染10000條數據在dom節點上</title>
</head>
<body>
    <ul id="root">

    </div>
    <script>
        function createOneHundredThousandData(){
            let arr = [];
            for(let i=0;i<100000;i++){
              arr.push({
                imgUrl:'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
                key:i
              })
            }
            return arr;
        }
        var beginTime = performance.now();
        console.log('beginTime',beginTime);
        let h = [];
        let data = createOneHundredThousandData()
        // 寫法1 原生js 的 for循環
        for(let i =0;i<data.length;i++){
            h.push('<li>' + '<img src="'+ data[i].imgUrl +'" \/>'+ 'current index ' + data[i].key + '<\/li>');
        }
        // 寫法2 數組自帶的map方法
        // h = data.map((item,index)=>'<li>' + '<img src="'+ item.imgUrl +'" \/>'+ 'current index ' + item.key + '<\/li>');
        document.getElementById('root').innerHTML = h.join('');
        document.addEventListener('DOMContentLoaded',function(){
          var endTime = performance.now();
          console.log('DOMContentLoaded endTime',endTime);
          var total = ((endTime - beginTime)/1000).toFixed(5);        
          console.log('DOMContentLoaded render 100000 items takes ' + total + ' 秒');
        });
        window.onload = function(){
          var endTime = performance.now();
          console.log('window.onload endTime',endTime);
          var total = ((endTime - beginTime)/1000).toFixed(5);        
          console.log('window.onload render 100000 items takes ' + total + ' 秒');  
        }
    </script>
</body>
</html>
複製代碼

chrome瀏覽器(版本 74.0.3729.169(正式版本) (64 位))控制檯運行結果以下html

beginTime 398.8050000043586
DOMContentLoaded endTime 9032.814999984112
DOMContentLoaded render 100000 items takes 8.63401 秒 
window.onload endTime 17766.104999987874
window.onload render 100000 items takes 17.36730 秒

複製代碼

也就是說,渲染包含十萬條記錄,每一條數據僅僅只有圖片和文字的簡單組合,就要花費將近17秒。頁面渲染完成以前,估計用戶早已不耐煩,關掉該頁面了。這仍是版本較新的chrome瀏覽器。換作其餘瀏覽器,可能效果更差。很顯然,傳統的方式確定不合格。前端

關於上述demo,有幾個問題能夠補充說明一下:react

  • 一、用innerHtml插入dom,而不是用document.createElement,document.appendChild,這二者性能上來講,innerHtml優點明顯chrome

  • 二、用數組[] 來緩存dom字符串,先push進來,最後再直接jion(''),將數組裏面每一項串聯成字符串,比一個一個字符串拼接的性能要強不少數組

  • 三、循環一個數組對象,能夠用for循環,也能夠用map,forEach等,數據量少的時候二者差異不大,在此例中,能夠看到map來循環十萬條數據時間性能稍遜於普通for循環。瀏覽器

  • 四、插入dom節點,還可使用克隆技術,文檔斷片createDocumentFragment,其根本目的在於儘量減小dom操做次數,從而使得重繪跟重排帶來的性能影響降到最低。若讀者有興趣深刻研究,能夠查閱《高性能JavaScript》(貓頭鷹頭像的封面)。緩存

  • 五、關於DOMContentLoaded事件和window.onload事件的對比,也是頁面渲染過程當中比較關鍵的,須要重點搞清楚的地方。 簡單來講,DOMContentLoaded 表示 dom 家在完成,通俗來講,就是dom標籤堆砌完畢,至於dom標籤引用什麼資源,有沒有請求加載完畢,那就無論了。好比在此例子中,十萬條數據img標籤堆上去,不須要等到img src指向的資源所有加載完就能夠觸發DOMContentLoaded;而window.onload事件則不同,要等到所有的src指向的資源所有加載完纔會被觸發。bash

DOMContentLoaded 也多用於關鍵路徑優化中(首屏操做優化),由於頁面dom加載完了就得給用戶提供一些交互。不能出現讓用戶看到UI界面卻作不了任何交互操做的狀況。app

2、解決卡頓問題之setTimeout

卡頓,多半是優於用戶發起一個操做,到頁面響應這個操做,把UI結果反饋給用戶這個時間存在明顯的延遲。給人不流暢的用戶體驗。dom

從JavaScript這門語言來看,它是單線程的,註定了同一時間,該線程只能處理一個任務,該任何處理完畢後才能處理下一個任務,你能夠理解爲串行執行。(更詳細更嚴謹的,能夠去深刻地瞭解JavaScript Event Loop)。因此,當頁面在執行渲染,或者很耗時JavaScript操做,該操做還沒完成,而此時你在頁面發起交互,就得不到及時的響應。

回到此題,咱們在渲染十萬掉數據的時候,要用到切片(有點相似react fiber的思想)。怎麼理解呢?就是把十萬掉數據分批次的渲染到頁面,這個批次任務必須放到異步回調(首批任務不用),這樣才能在後續的渲染中,把優先級讓出給執行隊列線程,當執行隊列空閒時,再回過頭來繼續取出異步回調裏面的切片來執行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>如何渲染10000條數據在dom節點上</title>
</head>
<body>
    <ul id="root">

    </div>
    <script>
        function createOneHundredThousandData(){
            let arr = [];
            for(let i=0;i<100000;i++){
              arr.push({
                imgUrl:'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
                key:i
              })
            }
            return arr;
        }
        var beginTime = performance.now();
        console.log('beginTime',beginTime);
        let h = [];
        let data = createOneHundredThousandData();
        // 先渲染100條數據
        let firstScreenData = data.splice(0,100); // 用數組的splice方法,截取後並修改原數組
        for(let i=0;i<100;i++){
          let li = document.createElement('li');
          let img = document.createElement('img');
          img.src  = firstScreenData[i].imgUrl;
          li.appendChild(img);
          let text = document.createTextNode(firstScreenData[i].key);
          // console.log('partialData[i].key',partialData[i].key);
          li.appendChild(text);
          document.getElementById('root').appendChild(li);
        }  
        // setTimeout 中的回調會在主線程空閒時被執行
        setTimeout(()=>{
          function renderHundred(n){
            // console.log('n=',n);
            // 每次渲染100條
            let partialData = data.splice(0,100);
            for(let i=0;i<100 && partialData.length>0;i++){
              let li = document.createElement('li');
              let img = document.createElement('img');
              img.src  = partialData[i].imgUrl;
              li.appendChild(img);
              let text = document.createTextNode(partialData[i].key);
              // console.log('partialData[i].key',partialData[i].key);
              li.appendChild(text);
              document.getElementById('root').appendChild(li);
            }            
            if(n){
              setTimeout(()=>{
                renderHundred(n-1);
              },50)
            }       
          }
          renderHundred(999);// 渲染除了首屏數據外的數據
        },1000); 
        document.addEventListener('DOMContentLoaded',function(){
          var endTime = performance.now();
          console.log('DOMContentLoaded endTime',endTime);
          var total = ((endTime - beginTime)/1000).toFixed(5);        
          console.log('DOMContentLoaded render 100000 items takes ' + total + ' 秒');
        });
        window.onload = function(){
          var endTime = performance.now();
          console.log('window.onload endTime',endTime);
          var total = ((endTime - beginTime)/1000).toFixed(5);        
          console.log('window.onload render 100000 items takes ' + total + ' 秒');  
        }
    </script>
</body>
</html>
複製代碼

運行結果以下:

beginTime 139.08000002265908
DOMContentLoaded endTime 193.2200000155717
DOMContentLoaded render 100000 items takes 0.05414 秒
window.onload endTime 207.63000001898035
window.onload render 100000 items takes 0.06855 秒

複製代碼

這個數據體會不出來什麼信息,能夠理解爲首個切片實行的耗時統計(後面還有999個切片沒有體現出來),可是在交互方面的體驗大大提高了。至於具體卡不卡頓的數據支撐,能夠在chrome控制檯performance模塊查看。你們有興趣的話,拷貝這份代碼嘗試一下。結論仍是比較樂觀的。

3、除了setTimeout,還有其餘的選擇嗎?

答案是,必須有。那就是requestAnimationFrame

window.requestAnimationFrame() 告訴瀏覽器——你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫。該方法須要傳入一個回調函數做爲參數,該回調函數會在瀏覽器下一次重繪以前執行

注意:若你想在瀏覽器下次重繪以前繼續更新下一幀動畫,那麼回調函數自身必須再次調用window.requestAnimationFrame()

從官方文檔能夠看到,這麼幾個關鍵字:「傳入一個回調函數」。 那麼是否是能夠用這個取代setTimeout?

如下截取部分代碼:

let data = createOneHundredThousandData();
let count = 0;
let totalLoop = 1000;// 渲染1000
function animatonCb(){
  console.log(count);
  let partialData = data.splice(0,100); // 用數組的splice方法,截取後並修改原數組
  for(let i=0;i<100 && partialData.length >=1;i++){
    let li = document.createElement('li');
    let img = document.createElement('img');
    img.src  = partialData[i].imgUrl;
    li.appendChild(img);
    let text = document.createTextNode(partialData[i].key);
    // console.log('partialData[i].key',partialData[i].key);
    li.appendChild(text);
    document.getElementById('root').appendChild(li);
  }
  if(count < totalLoop){
    count ++;
    requestAnimationFrame(animatonCb)
  }
}
requestAnimationFrame(animatonCb);
複製代碼

看下控制檯數據:

beginTime 249.32000000262633
0
DOMContentLoaded endTime 279.33499999926426
DOMContentLoaded render 100000 items takes 0.03001 秒
1
2
window.onload endTime 308.28500000643544
window.onload render 100000 items takes 0.05897 秒
複製代碼

咱們假如了循環次數count的打印,發現這個穿插在了DOMContentLoaded 和 onload事件中間。有興趣的童鞋能夠深刻了解requestAnimationFrame。

總之,這個requestAnimationFrame 也能實現咱們的需求。相比於setTimeout更好一點。

4、十萬條數據加載完成後呢?

上述兩個方案,也就是解決了如何渲染不卡頓的問題。本例中每條記錄dom結構不復雜,可能看起來效果還行。但實際業務場景確定是比這個更復雜。每次修改dom都會引發10萬條數據但重回重排,這樣性能方面確定也會有問題。

解決思路就是,監聽該元素是否在可視窗口IntersectionObserver

IntersectionObserver接口 (從屬於Intersection Observer API) 提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態的方法。祖先元素與視窗(viewport)被稱爲根(root)。 當一個IntersectionObserver對象被建立時,其被配置爲監聽根中一段給定比例的可見區域。一旦IntersectionObserver被建立,則沒法更改其配置,因此一個給定的觀察者對象只能用來監聽可見區域的特定變化值;然而,你能夠在同一個觀察者對象中配置監聽多個目標元素。

大概思路以下:

  • 設置總數據源,頁面內容數據存儲容器
  • 制定頁面內容數據存儲容器規則(假設存儲容器設置爲200條,一屏最多展現20條。那麼存儲容器能展現10屏幕的數據。
  • 當用戶滑到地6屏數據的時候,顯然前面5屏數據不在可視窗口,那你能夠將存儲容器的前3屏數據刪除。同時,再從總數據源取第11屏到第13屏數據。

後續有空了再詳細研究這塊。

5、結語

此類問題,也是bat這種大廠常常會問到的,知識點涵蓋也點廣,掌握好了後,對前端性能,卡頓這塊的理解會更透徹了。

相關文章
相關標籤/搜索