在大量数据渲染时,一次性将所有节点都渲染到页面上时,会发现页面加载和滚动的时候出现不可接受的卡顿。通过 Performance 开发人员工具可以看到,此时的性能开销,绝大部分都被 paint,render 消耗掉了。
如果数据节点比较简单,你可以选择将节点分片渲染到页面上:
<ul id="container"></ul>
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列的多层级列表。页面滚动时还是比较流程的。
本文的具体实现方式为 React。
虚拟滚动
要实现只渲染可视区域的关键,就是实现虚拟滚动,实现方式也比较简单:
- 最外层容器设置为一个合理的可视范围;
- 内层容器为所有数据高度之和;
- 垂直方向渲染一个占位元素,高度为滚动高度之和;
- 水平方向渲染一个占位元素,宽度为水平滚动之和;
Html 结构
<divref={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 屏的缓冲区,当视图移出缓冲区时,再渲染下一个缓冲区的节点。
如果虚拟滚动还是无法满足对性能的渴求,还有一些别的方案,例如,完全抛弃 DOM 节点,采用 canvas 自行进行元素的布局和绘制。
