后端一次性给你返回10万条数据,除了干一架之外就没别的办法了吗?

2,253 阅读4分钟

一次渲染10万条数据,另外还有搜索功能

这种事情要是发生在平时项目组内部前后端对接上

那。。。

image.png

不过,说出来你可能不信,我真的遇到过

没办法,甲方!

那就来吧。。。

下面分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);

进阶三:懒加载

  • 使用 IntersectionObservergetBoundingClientRect 获取滚动时,元素的相对位置
  • 防抖节流,优化滚动、搜索

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);

王者段位

虚拟列表

大前端百科全书性能优化专题之虚拟列表

对搜索算法进一步优化,比如二分法等,

image.png

如上图所示,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的通信机制来通知主进程, 比如模糊搜索等,

参考链接