JavaScript利用時間分片實現(xiàn)高性能渲染數(shù)據(jù)詳解
前言
在實際工作中,我們很少會遇到一次性需要向頁面中插入大量數(shù)據(jù)的情況,但是為了豐富我們的知識體系,我們有必要了解并清楚當遇到大量數(shù)據(jù)時,如何才能在不卡主頁面的情況下渲染數(shù)據(jù),以及其中背后的原理。
對于一次性插入大量數(shù)據(jù)的情況,一般有兩種做法:
- 時間分片
- 虛擬列表
本文作為開篇,著重來介紹如何使用時間分片的方式來渲染大量數(shù)據(jù),虛擬列表相關(guān)的內(nèi)容,參見「前端進階」高性能渲染十萬條數(shù)據(jù)(虛擬列表)
最粗暴的做法(一次性渲染)
我們先來看看最粗暴的做法,一次性將大量數(shù)據(jù)插入到頁面中:
<ul id="container"></ul>
// 記錄任務開始時間 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
我們對十萬條記錄進行循環(huán)操作,JS的運行時間為187ms,還是蠻快的,但是最終渲染完成后的總時間確是2844ms。
簡單說明一下,為何兩次console.log的結(jié)果時間差異巨大,并且是如何簡單來統(tǒng)計JS運行時間和總渲染時間:
- 在 JS 的Event Loop中,當JS引擎所管理的執(zhí)行棧中的事件以及所有微任務事件全部執(zhí)行完后,才會觸發(fā)渲染線程對頁面進行渲染
- 第一個console.log的觸發(fā)時間是在頁面進行渲染之前,此時得到的間隔時間為JS運行所需要的時間
- 第二個console.log是放到 setTimeout 中的,它的觸發(fā)時間是在渲染完成,在下一次Event Loop中執(zhí)行的
關(guān)于Event Loop的詳細內(nèi)容請參見這篇文章-->JS進階之從多線程到Event Loop全面梳理
依照兩次console.log的結(jié)果,可以得出結(jié)論:
對于大量數(shù)據(jù)渲染的時候,JS運算并不是性能的瓶頸,性能的瓶頸主要在于渲染階段
使用定時器
從上面的例子,我們已經(jīng)知道,頁面的卡頓是由于同時渲染大量DOM所引起的,所以我們考慮將渲染過程分批進行
在這里,我們使用setTimeout來實現(xiàn)分批渲染
<ul id="container"></ul>
//需要插入的容器 let ul = document.getElementById('container'); // 插入十萬條數(shù)據(jù) let total = 100000; // 一次插入 20 條 let once = 20; //總頁數(shù) let 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(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圖來看一下效果
我們可以看到,頁面加載的時間已經(jīng)非??炝?,每次刷新時可以很快的看到第一屏的所有數(shù)據(jù),但是當我們快速滾動頁面的時候,會發(fā)現(xiàn)頁面出現(xiàn)閃屏或白屏的現(xiàn)象
為什么會出現(xiàn)閃屏現(xiàn)象呢
首先,理清一些概念。FPS
表示的是每秒鐘畫面更新次數(shù)。我們平時所看到的連續(xù)畫面都是由一幅幅靜止畫面組成的,每幅畫面稱為一幀
,FPS
是描述幀
變化速度的物理量。
大多數(shù)電腦顯示器的刷新頻率是60Hz,大概相當于每秒鐘重繪60次,FPS
為60frame/s,為這個值的設定受屏幕分辨率、屏幕尺寸和顯卡的影響。
因此,當你對著電腦屏幕什么也不做的情況下,大多顯示器也會以每秒60次的頻率正在不斷的更新屏幕上的圖像。
為什么你感覺不到這個變化?
那是因為人的眼睛有視覺停留效應,即前一副畫面留在大腦的印象還沒消失,緊接著后一副畫面就跟上來了, 這中間只間隔了16.7ms(1000/60≈16.7),所以會讓你誤以為屏幕上的圖像是靜止不動的。
而屏幕給你的這種感覺是對的,試想一下,如果刷新頻率變成1次/秒,屏幕上的圖像就會出現(xiàn)嚴重的閃爍, 這樣就很容易引起眼睛疲勞、酸痛和頭暈目眩等癥狀。
大多數(shù)瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因為即使超過那個頻率用戶體驗也不會有提升。 因此,最平滑動畫的最佳循環(huán)間隔是1000ms/60,約等于16.6ms。
直觀感受,不同幀率的體驗:
- 幀率能夠達到 50 ~ 60 FPS 的動畫將會相當流暢,讓人倍感舒適;
- 幀率在 30 ~ 50 FPS 之間的動畫,因各人敏感程度不同,舒適度因人而異;
- 幀率在 30 FPS 以下的動畫,讓人感覺到明顯的卡頓和不適感;
- 幀率波動很大的動畫,亦會使人感覺到卡頓。
簡單聊一下 setTimeout 和閃屏現(xiàn)象
setTimeout
的執(zhí)行時間并不是確定的。在JS中,setTimeout
任務被放進事件隊列中,只有主線程執(zhí)行完才會去檢查事件隊列中的任務是否需要執(zhí)行,因此setTimeout
的實際執(zhí)行時間可能會比其設定的時間晚一些。- 刷新頻率受屏幕分辨率和屏幕尺寸的影響,因此不同設備的刷新頻率可能會不同,而
setTimeout
只能設置一個固定時間間隔,這個時間不一定和屏幕的刷新時間相同。
以上兩種情況都會導致setTimeout的執(zhí)行步調(diào)和屏幕的刷新步調(diào)不一致。
在setTimeout
中對dom進行操作,必須要等到屏幕下次繪制時才能更新到屏幕上,如果兩者步調(dià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)象。
我們使用requestAnimationFrame
來進行分批渲染:
<ul id="container"></ul>
//需要插入的容器 let ul = document.getElementById('container'); // 插入十萬條數(shù)據(jù) let total = 100000; // 一次插入 20 條 let once = 20; //總頁數(shù) let 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); 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);
看下效果
我們可以看到,頁面加載的速度很快,并且滾動的時候,也很流暢沒有出現(xiàn)閃爍丟幀的現(xiàn)象。
這就結(jié)束了么,還可以再優(yōu)化么?
當然~~
使用 DocumentFragment
先解釋一下什么是 DocumentFragment ,文獻引用自MDN
DocumentFragment,文檔片段接口,表示一個沒有父級文件的最小文檔對象。它被作為一個輕量版的Document使用,用于存儲已排好版的或尚未打理好格式的XML片段。最大的區(qū)別是因為DocumentFragment不是真實DOM樹的一部分,它的變化不會觸發(fā)DOM樹的(重新渲染) ,且不會導致性能等問題。
可以使用document.createDocumentFragment方法或者構(gòu)造函數(shù)來創(chuàng)建一個空的DocumentFragment
從MDN的說明中,我們得知DocumentFragments是DOM節(jié)點,但并不是DOM樹的一部分,可以認為是存在內(nèi)存中的,所以將子元素插入到文檔片段時不會引起頁面回流。
當append元素到document中時,被append進去的元素的樣式表的計算是同步發(fā)生的,此時調(diào)用 getComputedStyle 可以得到樣式的計算值。而append元素到documentFragment 中時,是不會計算元素的樣式表,所以documentFragment 性能更優(yōu)。當然現(xiàn)在瀏覽器的優(yōu)化已經(jīng)做的很好了,當append元素到document中后,沒有訪問 getComputedStyle 之類的方法時,現(xiàn)代瀏覽器也可以把樣式表的計算推遲到腳本執(zhí)行之后。
最后修改代碼如下:
<ul id="container"></ul>
//需要插入的容器 let ul = document.getElementById('container'); // 插入十萬條數(shù)據(jù) let total = 100000; // 一次插入 20 條 let once = 20; //總頁數(shù) let 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); 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);
到此這篇關(guān)于JavaScript利用時間分片實現(xiàn)高性能渲染數(shù)據(jù)詳解的文章就介紹到這了,更多相關(guān)JavaScript渲染數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js與jquery獲取父級元素,子級元素,兄弟元素的實現(xiàn)方法
本篇文章主要是對js與jquery獲取父級元素,子級元素,兄弟元素的實現(xiàn)方法進行了介紹,需要的朋友可以過來參考下,希望對大家有所幫助2014-01-01JS數(shù)組排序技巧匯總(冒泡、sort、快速、希爾等排序)
這篇文章主要介紹了JS數(shù)組排序技巧,實例匯總了JavaScript冒泡排序、sort排序、快速排序、希爾排序等,并附帶分析了sort排序的相關(guān)注意事項,需要的朋友可以參考下2015-11-11js獲取網(wǎng)頁可見區(qū)域、正文以及屏幕分辨率的高度
這篇文章主要介紹了js獲取網(wǎng)頁的各種高度,例如可見區(qū)域、正文以及屏幕分辨率的高度,需要的朋友可以參考下2014-05-05layui 監(jiān)聽彈窗關(guān)閉并刷新父級table的場景分析
這篇文章主要介紹了layui 監(jiān)聽彈窗關(guān)閉并刷新父級table的場景分析,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-07-07總結(jié)Javascript中數(shù)組各種去重的方法
相信大家都知道網(wǎng)上關(guān)于Javascript中數(shù)組去重的方法很多,這篇文章給大家總結(jié)Javascript中數(shù)組各種去重的方法,相信本文對大家學習和使用Javascript具有一定的參考借鑒價值,有需要的下面來一起看看。2016-10-10