虚拟滚动技术 --- 解决加载大量列表DOM导致页面卡顿

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟滚动技术 --- 解决加载大量列表DOM导致页面卡顿相关的知识,希望对你有一定的参考价值。

参考技术A 1、dom过多,占用过多的内存。
2、操纵dom时触发重排重绘,消耗浏览器性能。特别是每一次滚动事件将会让对应 DOM 中的所有元素重新渲染。
3、资源加载阻塞,比如js资源放在body之前、行内script阻塞、css加载会阻塞DOM树渲染(css并不会阻塞DOM树的解析)资源过大阻塞

至于第二个问题,详见 网红问题--前端性能优化(全流程) 或者 js回流和重绘

现在看来,这两点大条件是客观存在的,我们无法改变,所以这时候解决的方案就呼之欲出了,别让浏览器一次性渲染这么多元素,这里通常会对应三种做法来减少元素渲染。

这个方案是大家浏览到页面所常用的,通常在需要展示非常多行的数据时页面会采用分页的做法来分割数据,但在SQL结果集的场景下并不是通用方案,原因是虽然该方法减少了一次性所渲染的行数,但是如果查询的表列数非常多的话,还是有很大概率需要渲染非常多的元素,所以不是一个稳妥的选型。

该方案的解决方法是第一次只渲染所能承受范围内的数据量,当滚动条拖动接近底部(或右部)时,再去追加下一批所需要渲染的元素,该方案也是有一个明显的缺陷在于,无限地滚动下去必然会触及浏览器的性能瓶颈,而且所需要渲染的元素会越来越多,性能迟早会被拖垮。

其实答案已经隐藏在上面两种解决方案里面了,数据分页的方案是一次性渲染固定行数和列数的数据量,缺点是怕一次性的量就逼近上限。无限滚动的方案是想看更多数据的时候再继续渲染,不看就不渲染避免性能浪费,但缺点就在于只要一直触发“继续看”的操作,那么之前遗留的数据将会越来越多导致性能雪崩。

这时候可以把两个方案中和一下,既然在有限的视窗中我们只能看到一部分的数据,那么我们就通过计算可视范围内的单元格,这样就保证了每一次拖动,我们渲染的 DOM 元素始终是可控的,不会像数据分页方案怕一次性渲染过多,也不会发生无限滚动方案中的老数据堆积现象。接下来我们用一张图来表示虚拟滚动的表现形式。

根据图中我们可以看到,无论我们如何滚动,我们可视区域的大小其实是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值也就是可视范围内需要展示的内容,也就是图中的绿色区块,在可视区域之外的元素均可以不做渲染。

可以通过如下几步来实现虚拟滚动:

react列表置顶操作导致滚动问题的分析和解决,react dom-diff右移策略,overflowAnchor: ‘none‘遇到滚动锚定问题并且需要关闭行为

列表置顶操作导致滚动问题

需求分析

消息列表里面会有一些内容需要置顶,但是点击置顶后,列表直接滚到顶部不利于消息的批量操作。

复现效果

原因分析

区别参考于这篇文章https://www.cnblogs.com/yadiblogs/p/11129206.html

react 与 vue的diff区别
判断2个节点是否相同:vue认为className不一样,就不同,react则认为相同,只是属性不同,只需要更新其属性。
同一层级对比:Vue从两端至中间对比,react从左至右对比。react策略存在短板,如果一个集合只把最后一个移到第一个,react会移动前面所有节点,vue只移动最后一个节点到最前面。

key 具体是如何起作用的和为什么不能用 index 作为 key 值参考于这篇文章
https://bbs.huaweicloud.com/blogs/297739
为什么不能用 index 作为 key 值呢?
index 作为 key ,如果我们删除了一个节点,那么数组的后一项可能会前移,这个时候移动的节点和删除的节点就是相同的 key ,在react中,如果 key 相同,就会视为相同的组件,但这两个组件是不同的,这样就会出现很麻烦的事情,例如:序号和文本不对应等问题

所以一定要保证 key 的唯一性

那 key 具体是如何起作用的呢?

首先在 React 中只允许节点右移

因此对于上图中的转化,只会进行 A,C 的移动

则只需要对移动的节点进行更新渲染,不移动的则不需要更新渲染

解决方案

getSnapshotBeforeUpdate() 方法在最近一次渲染输出(提交到 DOM 节点)之前调用。

在 getSnapshotBeforeUpdate() 方法中,我们可以访问更新前的 props 和 state。

getSnapshotBeforeUpdate() 方法需要与 componentDidUpdate() 方法一起使用,否则会出现错误。

 // // 获取更新之前的快照
    // getSnapshotBeforeUpdate(prevProps, prevState) 
    //       // let top =  document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
    //       let top = document.querySelector('main').scrollTop;
    //       // console.log('list.scrollHeight - list.scrollTop;', document.body.scrollHeight, top);
    //       if (top !== 0) 
    //         // return document.body.scrollHeight - top;
    //         return top;

    //       
    //       return null
    //   
    
    //   componentDidUpdate(prevProps, prevState, snapshot) 
    //     console.log('snapshot', snapshot);
    //     //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    //    if(snapshot !== null) 
    //     const rootEle = document.querySelector('main');
    //     rootEle.scrollTop = this.state.top;
    //     this.listRef.current.scrollTop = snapshot
    //    
    //   

不足与补充

getSnapshotBeforeUpdate() 与 componentDidUpdate() 方法一起使用只能解决比较简单组件的场景,组件稍微复杂会有明显回弹效果是不佳的。问题根源不在于diff,diff只是个更新的手段和过程。最终都是要在页面上更新掉那些东西,滚动与否,不是diff和react控制的,是chrome控制的。

/* <main style=height: '900px', overflow: 'auto', overflowAnchor: 'none'  ref=this.listRef> */

overflowAnchor: ‘none’
CSS 属性提供一种退出浏览器滚动锚定行为的方法,该行为会调整滚动位置以最大程度地减少内容偏移。

默认情况下,在任何支持滚动锚定行为的浏览器中都将其启用。因此,仅当您在文档或文档的一部分中遇到滚动锚定问题并且需要关闭行为时,才通常需要更改此属性的值。

通过查阅 MDN 发现适配上不支持safari,也发现的确源码在safari上不会出现滚动bug。

源码



class List1 extends React.Component 
    constructor(props) 
        super(props)
        this.listRef= React.createRef();
    
    state= 
      list: [],
    ;
    
    componentDidMount() 
      const list = this.state.list.slice();
      for (let i = 0; i < 100  ; i++) 
        list.push(
          id: i,
          name: i.toString(),
        );
      
      this.setState( list );
    
    // // 获取更新之前的快照
    // getSnapshotBeforeUpdate(prevProps, prevState) 
    //       // let top =  document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
    //       let top = document.querySelector('main').scrollTop;
    //       // console.log('list.scrollHeight - list.scrollTop;', document.body.scrollHeight, top);
    //       if (top !== 0) 
    //         // return document.body.scrollHeight - top;
    //         return top;

    //       
    //       return null
    //   
    
    //   componentDidUpdate(prevProps, prevState, snapshot) 
    //     console.log('snapshot', snapshot);
    //     //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    //    if(snapshot !== null) 
    //     const rootEle = document.querySelector('main');
    //     rootEle.scrollTop = this.state.top;
    //     this.listRef.current.scrollTop = snapshot
    //    
    //   
  
    /**
     * 将列表中的项目置顶
     * @param index 要置顶的索引
     */
    toTop(index, e) 
      const list = this.state.list;
      const item = list[index];
      list.splice(index, 1);
      list.unshift(item);
      this.setState( list );
    
    render() 
      return (
        <div>
          /* <main style=height: '900px', overflow: 'auto', overflowAnchor: 'none'  ref=this.listRef> */
          <main style=height: '900px', overflow: 'auto'  ref=this.listRef>

          <hr />
          <ul  className='list' id='mylist'>
          <p ></p>

            this.state.list.map((item, index) => 
              return (
                <li key=item.id>
                  <span>item.name</span>
                  <span>
                    <button onClick=e => this.toTop(index, e)>置顶</button>
                  </span>
                </li>
              );
            )
          </ul>
          <hr />
          </main>
        </div>
      );
    
  


export default class App extends Component 
    render() 
        return (
            <div  >
              
               <div style=height: '20px'>hello</div>
               
                 <List1 />
            </div>
        )
    


以上是关于虚拟滚动技术 --- 解决加载大量列表DOM导致页面卡顿的主要内容,如果未能解决你的问题,请参考以下文章

无限滚动加载解决方案之虚拟滚动(上)

CCVirtualGridList - Cocos Creator 虚拟列表

Vue3 实现列表虚拟滚动

无限滚动加载最佳实践

解决 niceScroll 自适应DOM 高度变化

用react实现虚拟滚动组件