前端渲染优化 - 大量数据展示
在大量数据渲染时,一次性将所有节点都渲染到页面上时,会发现页面加载和滚动的时候出现不可接受的卡顿。通过 performance 开发人员工具可以看到,此时的性能开销,绝大部分都被 paint,render 消耗掉了。
如果数据节点比较简单,你可以选择将节点分片渲染到页面上:
const ul = document.getElementById('container');
const total = 100000;
const once = 20;
let index = 0;
function loop(curTotal, curIndex) {
if (curTotal <= 0) return false;
let pageCount = Math.min(curTotal, once);
requestAnimationFrame(() => {
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);
当节点较复杂时,如果将节点都渲染在页面上时,依然会造成页面卡顿。
所以在这种场景下,我们可以选择渲染可视区域的方式,先来看一个 demo 效果:
在图中渲染了一个 10k 行,100列的多层级列表。页面滚动时还是比较流畅的。
虚拟滚动
要实现只渲染可视区域的关键,就是实现虚拟滚动,实现方式也比较简单:
最外层容器设置为一个合理的可视范围;
内层容器为所有数据高度之和;
垂直方向渲染一个占位元素,高度为滚动高度之和;
水平方向渲染一个占位元素,宽度为水平滚动之和;
HTML 结构
<div
ref={scrollRef}
style={{ height: '500px', width: '500px',overflow: 'scroll' }}
>
<div style={{ height: `${nodeHeight * nodeYLength}px`, width: `${nodeWidth * nodeXLength}px` }}>
<div style={{ height: `${nodeHeight * srcollYLength}px` }} />
{/* render node element */}
</div>
</div>
滚动事件监听
当页面滚动时需要重新计算显示内容,通过暴露 x, y 给主程序使用:
import { useState, useEffect } from 'react';
/**
* scroll hook
* @param {*} scrollRef
* @returns {Object}
*/
export default function useScroll(scrollRef) {
const [position, setPosition] = useState({x: 0, y: 0});
useEffect(() => {
const handler = () => {
if (scrollRef.current) {
setPosition({
x: scrollRef.current.scrollLeft,
y: scrollRef.current.scrollTop,
});
}
};
if (scrollRef.current) {
scrollRef.current.addEventListener('scroll', handler, {
capture: false,
passive: true,
});
}
return () => {
if (scrollRef.current) {
scrollRef.current.removeEventListener('scroll', handler);
}
};
}, [scrollRef]);
return position;
}
有了 HTML 结构和滚动位置,只需要将源数据截取出需要渲染的数据就可以了。
全量计算
只有当用户变更行列配置时(如展开 / 收起行树)等时候需要对全量的行或者列进行枚举。但是由于树的操作不属于频繁调用的操作,而列数通常相对较少,因此也能达到不错的效果。
其他细节
层级提升:因为渲染区域会频繁操作 DOM , 我们可以使用 CSS 中的 will-change 属性,从原来的渲染层中独立出来,减少一部分浏览器渲染开销;
留出一定的缓冲区: 为了避免频繁更新组件的状态,减少 VDOM 比较的次数,一般需要预留四周 0.5 到 1 屏的缓冲区,当视图移出缓冲区时,再渲染下一个缓冲区的节点;
可视区变化:当视图大小发生变化时(特别是变大时),为了避免出现空白,我们需要重新计算可视范围;
数据占位:在实际项目中,数据一般都来自网络请求,而这些数据很可能是一个数据库中的记录。既然有如此大的数据量,那么数据请求上的优化也是一个重要的因素。一次性返回所有的数据可能在服务层和传输层造成较大的性能问题,由于数据库读取主键的速度比读取整个条目快得多,因此可以首先请求所有条目的主键,如 id,然后渲染一些条目的占位符,之后当用户滚动到某一个视图时,再通过主键集合请求该视图附近的数据。
如果虚拟滚动还是无法满足对性能的渴求,还有一些别的方案,例如,完全抛弃 DOM 节点,采用 canvas
自行进行元素的布局和绘制。
小结
前端渲染大量数据的时遇到性能瓶颈的原因可能有:
计算耗时:js 计算大量数据(10k+) 在时间复杂为 O(n²) 空间复杂度为O(1)耗时需要一秒左右,每增加一个空间复杂度系数,整体耗时就会翻倍;
在渲染大量节点时 paint 耗时,往往比 js 计算更耗时;
优化思路有以下几种:
-
避免时间复杂度 O(n²) 以及以上的遍历;
-
只渲染需要显示的节点;
-
优化显示方式:折叠显示内容、合并一定范围内节点显示(常用于网络图);
-
将需要重绘的节点提升层级,交互区域提升层级(如:拖拽中的节点);
-
使用 workers 开启多线程,把计算任务放在不同线程里。
以上就是所有内容,希望给你在渲染大量数据时提供一点思路。