<!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
卡頓,多半是優於用戶發起一個操做,到頁面響應這個操做,把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模塊查看。你們有興趣的話,拷貝這份代碼嘗試一下。結論仍是比較樂觀的。
答案是,必須有。那就是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更好一點。
上述兩個方案,也就是解決了如何渲染不卡頓的問題。本例中每條記錄dom結構不復雜,可能看起來效果還行。但實際業務場景確定是比這個更復雜。每次修改dom都會引發10萬條數據但重回重排,這樣性能方面確定也會有問題。
解決思路就是,監聽該元素是否在可視窗口IntersectionObserver
IntersectionObserver接口 (從屬於Intersection Observer API) 提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態的方法。祖先元素與視窗(viewport)被稱爲根(root)。 當一個IntersectionObserver對象被建立時,其被配置爲監聽根中一段給定比例的可見區域。一旦IntersectionObserver被建立,則沒法更改其配置,因此一個給定的觀察者對象只能用來監聽可見區域的特定變化值;然而,你能夠在同一個觀察者對象中配置監聽多個目標元素。
大概思路以下:
後續有空了再詳細研究這塊。
此類問題,也是bat這種大廠常常會問到的,知識點涵蓋也點廣,掌握好了後,對前端性能,卡頓這塊的理解會更透徹了。