<!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 位))控制檯運行結果以下javascript
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瀏覽器。換作其餘瀏覽器,可能效果更差。很顯然,傳統的方式確定不合格。html
關於上述demo,有幾個問題能夠補充說明一下:前端
DOMContentLoaded 也多用於關鍵路徑優化中(首屏操做優化),由於頁面dom加載完了就得給用戶提供一些交互。不能出現讓用戶看到UI界面卻作不了任何交互操做的狀況。java
卡頓,多半是優於用戶發起一個操做,到頁面響應這個操做,把UI結果反饋給用戶這個時間存在明顯的延遲。給人不流暢的用戶體驗。react
從JavaScript這門語言來看,它是單線程的,註定了同一時間,該線程只能處理一個任務,該任何處理完畢後才能處理下一個任務,你能夠理解爲串行執行。(更詳細更嚴謹的,能夠去深刻地瞭解JavaScript Event Loop)。因此,當頁面在執行渲染,或者很耗時JavaScript操做,該操做還沒完成,而此時你在頁面發起交互,就得不到及時的響應。chrome
回到此題,咱們在渲染十萬掉數據的時候,要用到切片(有點相似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模塊查看。你們有興趣的話,拷貝這份代碼嘗試一下。結論仍是比較樂觀的。緩存
答案是,必須有。那就是requestAnimationFrameapp
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更好一點。
上述兩個方案,也就是解決了如何渲染不卡頓的問題。本例中每條記錄dom結構不復雜,可能看起來效果還行。但實際業務場景確定是比這個更復雜。每次修改dom都會引發10萬條數據但重回重排,這樣性能方面確定也會有問題。
解決思路就是,監聽該元素是否在可視窗口IntersectionObserver
IntersectionObserver接口 (從屬於Intersection Observer API) 提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態的方法。祖先元素與視窗(viewport)被稱爲根(root)。 當一個IntersectionObserver對象被建立時,其被配置爲監聽根中一段給定比例的可見區域。一旦IntersectionObserver被建立,則沒法更改其配置,因此一個給定的觀察者對象只能用來監聽可見區域的特定變化值;然而,你能夠在同一個觀察者對象中配置監聽多個目標元素。
大概思路以下:
後續有空了再詳細研究這塊。
此類問題,也是bat這種大廠常常會問到的,知識點涵蓋也點廣,掌握好了後,對前端性能,卡頓這塊的理解會更透徹了。