React前端性能提升长列表优化解决方案

1.超长列表优化思路

1.1 概念

数据量较大且无法使用分页方式来加载的列表。比如淘宝的商品列表页,一次请求10个商品,一次请求10个商品和50个商品数据返回所需要的时间相差不大。但是却会多出4次的接口请求,造成资源浪费。

1.2 方案

  • 分片渲染(通过浏览器事件环机制,也就是 EventLoop,分割渲染时间)
  • 虚拟列表(只渲染可视区域)
1.2.1 进程与线程

进程是系统进行资源分配和调度的一个独立单位,一个进程内包含多个线程。常说的 JS 是单线程的,是指 JS 的主进程是单线程的。

1.2.2 浏览器中的 EventLoop

React前端性能提升长列表优化解决方案_第1张图片

1.2.3 运行机制

React前端性能提升长列表优化解决方案_第2张图片

1.2.4 宏任务包含:

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

1.2.5 微任务包含:

Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

1.3 思路

  1. 【分片渲染】 启用使用API setTimeout 分片渲染 每次渲染50条,进入宏任务列队进行页面渲染以提高效率。
  2. 开发一个【虚拟列表】组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X86iqJj4-1656642284323)(./img/img1.jpg)]

React前端性能提升长列表优化解决方案_第3张图片

长列表的渲染有以下几种情况:
1、列表项高度固定,且每个列表项高度相等
2、列表项高度固定不相等,但组件调用方可以明确的传入形如(index: number)=>number的getter方法去指定每个索引处列表项的高度
3、列表项高度不固定,随内容适应,且调用方无法确定具体高度

每种情况大致思路相似,计算出totalHeight撑起容器,并在滚动事件触发时根据scrollTop值不断更新startIndex以及endIndex,以此从列表数据listData中截取元素。

1.3.1 列表项高度固定,且每个列表项高度相等

核心代码

    
    private onScroll() {
        this.setState({scrollTop: this.container.current?.scrollTop || 0});
    }

    
    private calcListToDisplay(params: {
        scrollTop: number,
        listData: any[],
        itemHeight: number,
        bufferNumber: number,
        containerHeight: number,
    }) {
        const {scrollTop, listData, itemHeight, bufferNumber, containerHeight} = params;
        // 考虑到buffer
        let startIndex = Math.floor(scrollTop / itemHeight);
        startIndex = Math.max(0, startIndex - bufferNumber);  //计算出 带buffer 的数据起点 取最大值防止起点为负数
        const displayCount = Math.ceil(containerHeight / itemHeight);
        let lastIndex = startIndex + displayCount;  
        lastIndex = Math.min(listData.length, lastIndex + bufferNumber);  //计算出 带buffer 的数据终点,取最小值防止数据溢出

        return {
            data: listData.slice(startIndex, lastIndex + 1), //截取的数据
            offset: startIndex * itemHeight //顶部偏移量
        }
    }


    render() {
    const {itemHeight, listData, height: containerHeight, bufferNumber = 10} = this.props;
    const {scrollTop} = this.state;
    const totalHeight = itemHeight * listData.length;
    const { data: listToDisplay, offset } = 
        this.calcListToDisplay({scrollTop, listData, itemHeight, bufferNumber, containerHeight});
        return (
            
            
{ listToDisplay.map((item, index) => { return (
{item.text}
) }) }
) }
1.3.2 列表项高度固定不相等,但组件调用方可以明确的传入形如(index: number)=>number的getter方法去指定每个索引处列表项的高度

由于传入了Getter方法,相当于已知每个列表项的高度。我们可以维护一个数组posInfo来存储每个节点到容器顶部的距离,posInfo[i]即为第i项距离顶部的偏移量。

那么不考虑bufferNumber,只需要找出满足posInfo[k] < scrollTop,且posInfo[k+1] > scrollTop的k即可,由于posInfo一定是递增序列,可以采用二分法查找提高效率。

    
    private onScroll() {
        this.setState({scrollTop: this.container.current?.scrollTop || 0});
    }

    
    
    componentWillMount() {
        const { listData, heightGetter } = this.props;
        if (heightGetter instanceof Function) {
            this.initItemPosition(listData, heightGetter);
        }
    }
    
    private initItemPosition(listData: any[], heightGetter: heightGetter) {
        this.totalHeight = listData.reduce((total: number, item: any, index: number) => {
            const height = heightGetter(index);
            this.posInfo.push(total);
            return total + height;
        }, 0);
    }

    
    private getListToDisplay(params: {
        scrollTop: number;
        listData: any[];
        posInfo: number[];
        containerHeight: number;
        bufferNumber: number;
    }) {
        const { scrollTop, listData, posInfo, containerHeight, bufferNumber } = params;
        let startIndex = this.searchPos(posInfo, scrollTop);
        let lastIndex = listData.length - 1;
        const lastIndexDistance = containerHeight + scrollTop;
        for (let index = startIndex; index < listData.length; index++) {
            if (posInfo[index] >= lastIndexDistance) {
                lastIndex = index;
                break;
            }
        }
        // 考虑buffer
        startIndex = Math.max(0, startIndex - bufferNumber);
        lastIndex = Math.min(listData.length - 1, lastIndex + bufferNumber);
        return {
            data: listData.slice(startIndex, lastIndex + 1),
            offset: posInfo[startIndex]
        }
    }


    
    
    private searchPos(posInfo: number[], scrollTop: number) {
        const _binarySearchPos = (start: number, end: number): number => {
            if (end - start <= 1) {
                return start;
            }
            const mid = Math.ceil((start + end) / 2);
            if (posInfo[mid] === scrollTop) {
                return mid;
            } else if (posInfo[mid] < scrollTop) {
                if (posInfo[mid + 1] && posInfo[mid + 1] >= scrollTop) {
                    return mid;
                } else {
                    return _binarySearchPos(mid + 1, end);
                }
            } else {
                if (posInfo[mid - 1] && posInfo[mid - 1] <= scrollTop) {
                    return mid - 1;
                } else {
                    return _binarySearchPos(start, mid - 1);
                }
            }
        }
        return _binarySearchPos(0, posInfo.length - 1);
    }

    
    render() {
    const {itemHeight, listData, height: containerHeight, bufferNumber = 10} = this.props;
    const {scrollTop} = this.state;
    const totalHeight = itemHeight * listData.length;
    const { data: listToDisplay, offset } = 
        this.calcListToDisplay({scrollTop, listData, itemHeight, bufferNumber, containerHeight});
        return (
            
            
{ listToDisplay.map((item, index) => { return (
{item.text}
) }) }
) } { return listData[index].height }} listData={listData} />
1.3.3 列表项高度不固定,随内容适应,且调用方无法确定具体高度

核心代码

    
    

    componentWillMount() {
    
        this.heightCache = new Array(this.props.listData.length).fill(this.props.fuzzyItemHeight || 30);
    }

  

    
    onMount(index: number, height: number) {
        if (index > this.lastCalcIndex) {
                
            this.heightCache[index] = height;  // heightCache数组存储已挂载过的列表项的高度
            this.lastCalcIndex = index;  //lastCalcIndex记录最后一个已挂载节点的索引
            this.lastCalcTotalHeight += height;  //lastCalcTotalHeight记录已挂载节点的全部高度和
            //趋于准确
            this.totalHeight = this.lastCalcTotalHeight + (this.props.listData.length - 1 - this.lastCalcIndex) * (this.props.fuzzyItemHeight || 30);
        }
    }





    private getListToDisplay(params: {
        scrollTop: number;
        containerHeight: number;
        itemHeights: number[];
        bufferNumber: number;
        listData: any[];
    }) {
        const { scrollTop, containerHeight, itemHeights, bufferNumber, listData } = params;
        let calcHeight = itemHeights[0]; //初始化(已遍历的节点总高度) 值为 第一个已遍历节点的高度 
        let startIndex = 0;
        let lastIndex = 0;
        const posInfo = []; // posInfo记录各节点到顶部的距离
        posInfo.push(0);
        for (let index = 1; index < itemHeights.length; index++) {
            //已遍历节点的总高度 > scrollTop滚动距离
            if (calcHeight > scrollTop) {
                startIndex = index - 1;
                break;
            }
            posInfo.push(calcHeight);
            calcHeight += itemHeights[index];
        }
        for (let index = startIndex; index < itemHeights.length; index++) {
            if (calcHeight > scrollTop + containerHeight) {
                lastIndex = index;
                break;
            }
            calcHeight += itemHeights[index];
        }
        startIndex = Math.max(0, startIndex - bufferNumber);
        lastIndex = Math.min(itemHeights.length - 1, lastIndex + bufferNumber);
        return {
            data: listData.slice(startIndex, lastIndex + 1),
            offset: posInfo[startIndex]
        }
    }


    
    render() {
    const { height: containerHeight, listData, bufferNumber = 10 } = this.props;
    const { scrollTop } = this.state;
    
    const { data: _listToDisplay, offset } = this.getListToDisplay({ scrollTop, listData, itemHeights: this.heightCache, containerHeight, bufferNumber });

    return (
        
{ _listToDisplay.map((item, index) => { return ( {/* */}
{item.text}
) }) }
) }

1.4 推荐

react-virtualized
如果使用react开发,可以使用antdesign官网推荐的组件,结合 react-virtualized 实现滚动加载无限长列表,带有虚拟化(virtualization)功能,能够提高数据量大时候长列表的性能。

你可能感兴趣的:(前端,react.js,javascript,性能优化,html)