vlambda博客
学习文章列表

前端渲染优化 - 大量数据展示

在大量数据渲染时,一次性将所有节点都渲染到页面上时,会发现页面加载和滚动的时候出现不可接受的卡顿。通过 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 开启多线程,把计算任务放在不同线程里。


以上就是所有内容,希望给你在渲染大量数据时提供一点思路。