一次渲染10万条数据,另外还有搜索功能
这种事情要是发生在平时项目组内部前后端对接上
那。。。
不过,说出来你可能不信,我真的遇到过
没办法,甲方!
那就来吧。。。
下面分level给大家分享一下解决方案
青铜段位
干就完了!
直接渲染
const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()
然后就是卡成狗
黄金段位
setTimeout分页渲染 - 时间分片
时间分片
旨在把一个运行时间比较长的任务分解成一块一块比较小的任务,分块去执行,因为超过 50ms 的任务就会被认为是 long task,用户就能感知到渲染卡顿和交互的卡顿,所以我们可以缩短函数的连续执行时间。
浏览器渲染时机
除去特殊情况,页面的渲染会在微任务队列清空后,宏任务执行前,所以我们可以让推入主执行栈的函数执行到一定时间就去休眠,然后在渲染之后的宏任务里面叫醒他,这样渲染或者用户交互都不会卡顿了!
const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)
const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}
进阶一:使用 requestAnimationFrame
与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。
如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。
我们使用requestAnimationFrame来进行分批渲染
进阶二:使用 DocumentFragment
DocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。
当append元素到document中时,被append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。 而append元素到documentFragment 中时,是不会计算元素的样式表,所以documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当append元素到document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
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);
进阶三:懒加载
- 使用
IntersectionObserver、getBoundingClientRect获取滚动时,元素的相对位置 - 防抖节流,优化滚动、搜索
IntersectionObserver API是异步的,不随着目标元素的滚动同步触发,性能消耗极低。
const box = document.querySelector('.box');
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.isIntersecting) {
console.log('进入可视区域');
}
})
},
{
threshold: [0, 0.5],
root: document.querySelector('.container'),
rootMargin: "10px 10px 30px 20px",
});
intersectionObserver.observe(box);
王者段位
虚拟列表
对搜索算法进一步优化,比如二分法等,
如上图所示,low和high代表数组的两边下标,mid代表数组的中间下标。
- 若目标值比中间值大,即目标值在mid与high之间,就修改low的值。再对比中间值。
- 若目标值比中间值小,即目标值在low与mid之间,就修改high的值。再对比中间值。
上述就是二分查找的过程,那它的时间复杂度怎么求呢❓
假设数组的长度为n,那么查找一次后长度变为n/2,再查找一次后长度变为n/4,以此类推,最坏情况下,n/2^k为空,查找停止。于是我们有以下的公式:
n * n/2 * n/4 * n/8 * n/2^k ·····
以上是一个等比数列,n / 2^k = 1时,k就是查找的次数。即k=log2n,所以时间复杂度为 O(logn),这是一种非常高效率的算法。
多线程webworker
我们还可以通过web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算, 计算完成后再通过web worker的通信机制来通知主进程, 比如模糊搜索等,
参考链接