后端一次性返回了10w条数据,前端该如何处理?长列表性能优化
这个问题其实是考察面试者对性能优化的理解。我们知道,对于大量数据渲染的时候,JS运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段,所以页面的卡顿是由于同时渲染大量DOM所引起的。
前端长数据渲染方案:
一、一次性渲染
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><ul id="container"></ul><script>// 模拟十万条数据let total =100000;// 直接渲染let ul = document.getElementById('container');constrenderList=()=>{for(let index =0; index < total; index++){const li = document.createElement("li");
li.innerHTML =`${index}---${~~(Math.random()* total)}`;
ul.appendChild(li);}};renderList()</script></body></html>
二、使用定时器进行分页渲染
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>Document</title></head><body><ul id="container"></ul><script>// 模拟十万条数据let total =100000;let page =0;// 每页插入limit条数据const limit =200;//总页数const totalPage = Math.ceil(total / limit);// Math.ceil 1.1 => 2let ul = document.getElementById("container");//循环加载数据constrenderList=(page)=>{if(page >= totalPage)return;setTimeout(()=>{for(let i = page * limit; i < page * limit + limit; i++){const li = document.createElement("li");
li.innerHTML =`${i}---${~~(Math.random()* total)}`;
ul.appendChild(li);}renderList(page +1);},0);};renderList(page);</script></body></html>
简单聊一下 setTimeout 和闪屏现象
setTimeout的执行时间并不是确定的。在JS中, setTimeout任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否需要执行,因此 setTimeout的实际执行时间可能会比其设定的时间晚一些。
刷新频率受屏幕分辨率和屏幕尺寸的影响,大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次,因此不同设备的刷新频率可能会不同,而 setTimeout只能设置一个固定时间间隔,这个时间不一定和屏幕的刷新时间相同。
以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。
在 setTimeout中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧闪屏现象
三、使用 requestAnimationFrame
使用 requestAnimationFrame 代替 setTimeout,减少了重排的次数, requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机,极大提高了性能
如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是, requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。
constrenderList=(page)=>{if(page >= totalPage)return;//使用 requestAnimationFrame 代替 setTimeout,减少重排次数requestAnimationFrame(()=>{for(let i = page * limit; i < page * limit + limit; i++){const li = document.createElement("li");
li.innerHTML =`${i}---${~~(Math.random()* total)}`;
ul.appendChild(li);}renderList(page +1);});};renderList(page);
四、使用 DocumentFragment
先解释一下什么是 DocumentFragment
- DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。
- 在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中。
- 所以将子元素插入到文档片段时不会引起【页面回流】(对元素位置和几何上的计算)。
因此,使用文档片段通常会带来更好的性能。
constrenderList=(page)=>{if(page >= totalPage)return;requestAnimationFrame(()=>{let fragment = document.createDocumentFragment();for(let i = page * limit; i < page * limit + limit; i++){const li = document.createElement("li");
li.innerHTML =`${i}---${~~(Math.random()* total)}`;
fragment.appendChild(li)}
ul.appendChild(fragment)renderList(page +1);});};renderList(page);
五、懒渲染/延迟渲染scrollTop、getBoundingClientRect
要获取用户的滚动位置,我们可以在列表末尾添加一个空节点空白。每当视口出现空白时,就意味着用户已经滚动到网页底部,这意味着我们需要继续渲染数据。
同时,我们可以使用getBoundingClientRect来判断空白是否在页面底部。
使用 Vue 的示例代码:
<script setup lang="ts">import{ onMounted, ref, computed }from'vue'constgetList=()=>{// code as before}const container = ref<HTMLElement>()// container elementconst blank = ref<HTMLElement>()// blank elementconst list = ref<any>([])const page =ref(1)const limit =200const maxPage =computed(()=> Math.ceil(list.value.length / limit))// List of real presentationsconst showList =computed(()=> list.value.slice(0, page.value * limit))consthandleScroll=()=>{if(page.value > maxPage.value)returnconst clientHeight = container.value?.clientHeight
const blankTop = blank.value?.getBoundingClientRect().top
if(clientHeight === blankTop){
page.value++}}onMounted(async()=>{const res =awaitgetList()
list.value = res
})</script><template><div id="container" @scroll="handleScroll" ref="container"><div class="sunshine" v-for="(item,index) in showList":key="index"><span>{{ index }}</span><img :src="item.src"/></div><div ref="blank"></div></div></template>
六、在vue里面:长列表性能优化
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
exportdefault{data:()=>({users:{}}),asynccreated(){const users =await axios.get("/api/users");this.users = Object.freeze(users);}};
版权归原作者 C+ 安口木 所有, 如有侵权,请联系我们删除。