在大量数据渲染时,一次性将所有节点都渲染到页面上时,会发现页面加载和滚动的时候出现不可接受的卡顿。通过 Performance 开发人员工具可以看到,此时的性能开销,绝大部分都被 paint,render 消耗掉了。

如果数据节点比较简单,你可以选择将节点分片渲染到页面上:

  1. <ul id="container"></ul>
  1. const ul = document.getElementById('container');
  2. const total = 100000;
  3. const once = 20;
  4. let index = 0;
  5. function loop(curTotal, curIndex) {
  6. if (curTotal <= 0) return false;
  7. let pageCount = Math.min(curTotal, once);
  8. requestAnimationFrame(() => {
  9. let fragment = document.createDocumentFragment();
  10. for (let i = 0; i < pageCount; i++) {
  11. let li = document.createElement('li');
  12. li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
  13. fragment.appendChild(li);
  14. }
  15. ul.appendChild(fragment);
  16. loop(curTotal - pageCount, curIndex + pageCount);
  17. })
  18. }
  19. loop(total, index);

效果图

当节点较复杂时,如果将节点都渲染在页面上时,依然会造成页面卡顿。

所以在这种场景下,我们可以选择渲染可视区域的方式,先来看一个 demo 效果:

ky34qchgMZ.gif

在图中渲染了一个 10k 行,100列的多层级列表。页面滚动时还是比较流程的。

本文的具体实现方式为 React。

虚拟滚动

要实现只渲染可视区域的关键,就是实现虚拟滚动,实现方式也比较简单:

  • 最外层容器设置为一个合理的可视范围;
  • 内层容器为所有数据高度之和;
  • 垂直方向渲染一个占位元素,高度为滚动高度之和;
  • 水平方向渲染一个占位元素,宽度为水平滚动之和;

Html 结构

  1. <div
  2. ref={scrollRef}
  3. style={{ height: '500px', width: '500px',overflow: 'scroll' }}
  4. >
  5. <div style={{ height: `${nodeHeight * nodeYLength}px`, width: `${nodeWidth * nodeXLength}px` }}>
  6. <div style={{ height: `${nodeHeight * srcollYLength}px` }} />
  7. {/* render node element */}
  8. </div>
  9. </div>

滚动事件监听

当页面滚动时需要重新计算显示内容,通过暴露 x, y 给主程序使用:

  1. import { useState, useEffect } from 'react';
  2. /**
  3. * scroll hook
  4. * @param {*} scrollRef
  5. * @returns {Object}
  6. */
  7. export default function useScroll(scrollRef) {
  8. const [position, setPosition] = useState({x: 0, y: 0});
  9. useEffect(() => {
  10. const handler = () => {
  11. if (scrollRef.current) {
  12. setPosition({
  13. x: scrollRef.current.scrollLeft,
  14. y: scrollRef.current.scrollTop,
  15. });
  16. }
  17. };
  18. if (scrollRef.current) {
  19. scrollRef.current.addEventListener('scroll', handler, {
  20. capture: false,
  21. passive: true,
  22. });
  23. }
  24. return () => {
  25. if (scrollRef.current) {
  26. scrollRef.current.removeEventListener('scroll', handler);
  27. }
  28. };
  29. }, [scrollRef]);
  30. return position;
  31. }

有了html结构和滚动位置,只需要将源数据截取出需要渲染的数据就可以了。

全量计算

只有当用户变更行列配置时(如展开 / 收起行树)等时候需要对全量的行或者列进行枚举。但是由于树的操作不属于频繁调用的操作,而列数通常相对较少,因此也能达到不错的效果。

其他细节

  • 层级提升:因为渲染区域会频繁操作 Dom , 我们可以使用CSS中的 will change 属性,从原来的渲染层中独立出来,减少一部分浏览器渲染开销;
  • 留出一定的缓冲区。 为了避免频繁更新组件的状态,减少 VDOM 比较的次数,一般需要预留四周 0.5 到 1 屏的缓冲区,当视图移出缓冲区时,再渲染下一个缓冲区的节点。

如果虚拟滚动还是无法满足对性能的渴求,还有一些别的方案,例如,完全抛弃 DOM 节点,采用 canvas 自行进行元素的布局和绘制。