谈谈前端渲染的性能问题:时间分片,缓冲池,worker…

1. 先从十万条列表说起

本章例子引用云中桥的本站博客文章

最近经常在一些技术博客中看到有人提及十万条列表怎么才能不卡住主页面来进行渲染,这就是经典的dom渲染优化问题,这里直接用原博的例子,写的非常详细了。

<ul id="container"></ul>
复制代码
// 记录任务开始时间
let now = Date.now();
// 插入十万条数据
const total = 100000;
// 获取容器
let ul = document.getElementById('container');
// 将数据插入容器中
for (let i = 0; i < total; i++) {
    let li = document.createElement('li');
    // ~~ 代表 x < 0 ? Math.ceil(x) : Math.floor(x);
    li.innerText = ~~(Math.random() * total)
    ul.appendChild(li);
}

console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
    console.log('总运行时间:',Date.now() - now);
},0)
复制代码

运行结果如下:

image-20210530174327401

整个页面的表现就是白屏等待很久才看到首屏的数据出现。可以看到对于这种没有任何算法甚至没有一些数据处理逻辑的js运算来说,并没有什么性能瓶颈,卡顿的原因主要在于大批量高频率的操作dom进行渲染。

对症下药,既然是同时渲染太卡顿,那就分批进行渲染即可,也就是时间分片的方法,最常用的就是setTimeout异步渲染。

//需要插入的容器
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);
    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);
复制代码

当然如果每条列表的dom结构比较复杂,都渲染出来后浏览器页面能不能承受住如此大批量的dom也是个问题,后续也有虚拟列表的方案,其实是以前的数据请求懒加载在渲染层面上的另一种形式的应用,这里不做详细介绍,因为当页面结构不是简单的列表而是复杂的树结构等图形,这种方式的虚拟计算基本是不现实的。

2. 项目中经常遇到的websocket处理

其实上一章这种十万条列表的渲染正常项目中也基本不会出现,用用分页之类的就可以解决。毕竟十万条列表的滚动条谁扛得住~~

目前我在项目中经常遇到的渲染性能问题就是websocket的持续大批量,高频率推送数据,dom渲染会卡死,注意这里的数据不像上一章列表的数据量是开始就请求确定的,这里推送的数据是动态不间断的,因此可以引入数据缓冲池,

let bufferPool = [];    // 用来缓存一段时间的数据
let bufferTimer;
// 接收websocket数据的函数
function onMsg(data) {
  bufferPool.push(data);
  //每次都把当前的数据进行push到list
  if(!bufferTimer){
    bufferTimer = setTimeout(()=>{
      // 一次性处理缓冲数据
      render(bufferPool);
      bufferPool = [];
      socketTimer = null;
    }, 500);
  }
};
ws.on('message', onMsg);
复制代码

依然是用万能的setTimeout,也就是把时间分片的思想用到了数据推送上,分批渲染推送过来的数据。

但是缓冲池还有个问题,假如就是这500ms内推送了大批量的数据,同时渲染还是有比较大的问题,因此可以在渲染dom时候再加上一个按数据长度分片的setTimeout,也就是再应用渲染的时间分片,如果数据长度大于某个长度,就同第一章一样分片渲染。

function render(bufferPool) {
  if (bufferPool.length > 100) {
  	// 时间分片渲染
  	...
  } else {
  	// 一次性循环渲染
  	...
  }
}
复制代码

当然这里缓冲池的做法看起来非常的不优雅…如果你的项目中有用rxjs,此时就可以派上用场了,rxjs在处理数据流方面真是神器。

Rx.Observable
    .fromEvent(ws, 'message')
    .bufferTime(1000)
    .subscribe(bufferPool => render(bufferPool))
复制代码

3. 你真的需要引入web worker么?

继续谈性能优化,就不得不提到最近几年比较火的web worker了,没有什么神秘的,看看MDN文档的例子很快就明白了:

var worker = new Worker("js/worker.js");
//发送data至worker中
worker.postMessage(data);
//worker处理完数据后的回调函数
worker.onmessage = function (e) {
	var data = e.data;  //worker处理后返回的数据
}

// js/worker.js
onmessage = function (e) {
	var data = e.data; //worker接受数据
	// 接受到数据后,进行处理
    ...
	postMessage(data); //发送处理后的数据
}
复制代码

可以跟js定时器对比下。

setTimeout本质还是单线程,通过事件循环实现并发,但是这种并发其实是一种假并发,定时器中的事件仍然会阻塞线程,也就是会导致在进行某些复杂计算和渲染时操作页面感到卡顿,电脑的多核CPU形同虚设,不能得到有效的利用。

web worker则是正儿八经的多线程,我们可以把一些js数据处理放到worker的线程中,此时主线程中不会受任何影响,操作页面很流畅。

回到上一节的例子,虽然对websocket进行了缓冲,对渲染进行了分片处理的策略,但是假如推送的数据还要经过一定的处理,那么会长期占用js单线程的资源,如果想推送数据并且处理数据的时候不影响当前页面的正常使用性能,考虑引入web worker,在worker里订阅websocket,处理数据。

我目前的项目推送的数据量较大并且原始数据还要经过一系列的筛选处理拼接,但这种逻辑复杂的处理就有必要使用worker么?其实关键在于进行复杂处理的时候是否需要操作页面,假如你的场景是点击请求数据等待处理渲染,用户其实对这种是有心理预期的,也不会在这么短的时间内去滑动页面做操作,那么根本不需要使用worker。而像我目前是一个持续的websocket更新处理完全是一个后台的行为,用户并不能及时感知,大批量处理就不能影响用户的当前操作,此时放到worker线程中进行处理就再适合不过了。

4. 参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享