背景:在實際工作中浸颓,我們很少會遇到一次性需要向頁面中插入大量數(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)