1.超长列表优化思路

1.1 概念

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

1.2 方案

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

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

1.2.2 浏览器中的 EventLoop

1.2.3 运行机制

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)]

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

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

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

核心代码

    <!-- 为容器绑定 scrollTop,且实时更新scrollTop值  -->private onScroll() {this.setState({scrollTop: this.container.current?.scrollTop || 0});}<!-- 计算 数据截取始末index  -->private calcListToDisplay(params: {scrollTop: number,listData: any[],itemHeight: number,bufferNumber: number,containerHeight: number,}) {const {scrollTop, listData, itemHeight, bufferNumber, containerHeight} = params;// 考虑到bufferlet 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 (<!-- 外层容器 --><div ref={this.container} onScroll={this.onScroll} style={{height: `${containerHeight}px`, overflow: 'auto'}}><!-- 计算所有数据高度,用于显示滚动条 --><div className="virtual-list-wrapper" style={{height: `${totalHeight}px`}}><!-- 展示内容 使用 transform 时刻保持在屏幕中央 --><div style={{transform: `translateY(${offset}px)`}}>{listToDisplay.map((item, index) => {return (<ListItem key={item.key ? item.key: index}><img src={item.img}/><div>{item.text}</div></ListItem>)})}</div></div></div>)}<!-- 调用组件 --><VirtualList height={300} itemHeight={38} listData={generateList()} />
1.3.2 列表项高度固定不相等,但组件调用方可以明确的传入形如(index: number)=>number的getter方法去指定每个索引处列表项的高度

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

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

    <!-- 为容器绑定 scrollTop,且实时更新scrollTop值  -->private onScroll() {this.setState({scrollTop: this.container.current?.scrollTop || 0});}<!-- 调用初始化高度 -->componentWillMount() {const { listData, heightGetter } = this.props;if (heightGetter instanceof Function) {this.initItemPosition(listData, heightGetter);}}<!-- 使用 posInfo 存储每个节点到容器顶部的距离 -->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);}<!-- 截取数据 获取 lastIndex -->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;}}// 考虑bufferstartIndex = Math.max(0, startIndex - bufferNumber);lastIndex = Math.min(listData.length - 1, lastIndex + bufferNumber);return {data: listData.slice(startIndex, lastIndex + 1),offset: posInfo[startIndex]}}<!-- 使用二分法得到开始的index  --><!-- 即找出满足posInfo[k] < scrollTop,且posInfo[k+1] > scrollTop的k即可 -->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 (<!-- 外层容器 --><div ref={this.container} onScroll={this.onScroll} style={{height: `${containerHeight}px`, overflow: 'auto'}}><!-- 计算所有数据高度,用于显示滚动条 --><div className="virtual-list-wrapper" style={{height: `${totalHeight}px`}}><!-- 展示内容 使用 transform 时刻保持在屏幕中央 --><div style={{transform: `translateY(${offset}px)`}}>{listToDisplay.map((item, index) => {return (<ListItem key={item.key ? item.key: index}><img src={item.img}/><div>{item.text}</div></ListItem>)})}</div></div></div>)}<!-- 调用组件 --><VirtualList height={300} heightGetter={(index) => { return listData[index].height }} listData={listData} />
1.3.3 列表项高度不固定,随内容适应,且调用方无法确定具体高度

核心代码

    <!-- 设置默认虚拟 高度 fuzzyItemHeight --><!-- 由于无法得知节点具体高度,可以通过给出一个模糊高度fuzzyItemHeight来初始化一个并不准确的高度撑起容器。接着在滚动过程中,item组件挂载后可以得到准确的高度,此时更新totalHeight,使totalHeight趋于准确-->componentWillMount() {<!-- 所有元素虚拟高度集合 不准确-->this.heightCache = new Array(this.props.listData.length).fill(this.props.fuzzyItemHeight || 30);}<!-- 子组件命周期componentDidMount内更新totalHeight-->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);}}<!-- 计算可见节点
遍历已缓存的节点高度,calcHeight记录已遍历的节点总高度,直到calcHeight > scrollTop,记录当前节点索引为startIndex,同理找出calcHeight > scrollTop + containerHeight的节点索引为endIndex。与此同时,posInfo记录各节点到顶部的距离,以便直接给出偏移量offset = posInfo[startIndex] -->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;<!-- scrollTop 滚动距离itemHeights 需要挂在的元素 高度 默认为 30 若挂载过 则会更新高度值 Arr containerHeight 容器高度 【固定】bufferNumber 缓冲元素数-->const { data: _listToDisplay, offset } = this.getListToDisplay({ scrollTop, listData, itemHeights: this.heightCache, containerHeight, bufferNumber });return (<div ref={this.container} onScroll={this.onScroll} style={{ height: `${containerHeight}px`, overflow: 'auto' }}><div className="virtual-list-wrapper" style={{ height: `${this.totalHeight}px` }}><div style={{ transform: `translateY(${offset}px)`, willChange: 'transform' }}>{_listToDisplay.map((item, index) => {return (<ListItem key={item.key ? item.key : index} onMount={this.onMount.bind(this, listData.indexOf(item))}>{/* <img src={item.img} /> */}<div>{item.text}</div></ListItem>)})}</div></div></div>)}

1.4 推荐

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

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

  1. 【前端性能优化】长列表优化

    1 什么是长列表? 1.1  概念 前端的业务开发中会遇到一些数据量较大且无法使用分页方式来加载的列表,我们一般把这种列表叫做长列表. 1.2 参考案例 比如淘宝网的商品列表页,一个手机屏可以容纳10 ...

  2. 【Day12】整个前端性能提升大致分几类

    整个前端性能提升大致分几类 网站性能提升 1.静态资源的优化 2.接口访问的优化 3.页面渲染速度的优化 网站性能提升 1.静态资源的优化 主要是减少静态资源的加载时间,主要包括 html.js.cs ...

  3. 前端性能和加载体验优化实践(附:PWA、离线包、内存优化、预渲染)

    一.背景:页面为何会卡? 1.1 等待时间长(性能) 项目本身包/第三方脚本比较大. JavaScript 执行阻塞页面加载. 图片体积大且多. 特别是对于首屏资源加载中的白屏时间,用户等待的时间就越 ...

  4. Rax SSR 完成6倍 React 渲染性能提升

    什么是 SSR SSR 的全称是 Server Side Rendering,对应的中文名是:服务器端渲染,顾名思义是将渲染的工作放在 Server 端进行. 而与之对应的是 CSR ,客户端渲染,也 ...

  5. 魅蓝u20android版本,魅族魅蓝U20刷机包 Flyme 6 稳定版系统发布 性能提升 超长待机 全面优化...

    rom更新记录 功能调整 每个功能从创造到落地都花费了巨大心血,只为给魅友带来更多惊喜.由于新功能在各个机型上的适配会存在一定时间差,未找到新功能的魅友请耐心等候. 系统 · 全面提升系统稳定性,同时 ...

  6. CocosCreator无尽循环列表,长列表优化drawcall,scrollview列表优化

    我这里只实现纵向滑动列表,横向的话直接修改一下就好 cocos creator 2.4.4 参考链接 CocosCreator无尽循环列表,ScrollView优化_zakerhero的博客-CSDN ...

  7. React项目实战之租房app项目(四)长列表性能优化城市选择模块渲染列表

    前言 目录 前言 一.长列表性能优化 1.1 概述 1.2 懒渲染 1.3 可视区渲染(React-virtualized) 二.react-virtualized组件 2.1 概述 2.2 基本使用 ...

  8. 判断 小程序 是否 滚动到页面底部 scrolltolower_微信小程序长列表性能优化——recycle-view

    背景: 第七次人口普查项目使用是微信小程序原生框架,组件是根据用户需求由项目组前端组组长封装完成的.采集小程序正式登记首页列表页面,根据腾讯老哥在sentry上的监控可以看出,列表页面前端性能比较差, ...

  9. Web前端性能优化思路

    本文旨在整理常见Web前端性能优化的思路,可供前端开发参考.因为力求精简,限于篇幅,所以并未详述具体实施方案. 基于现代Web前端框架的应用,其原理是通过浏览器向服务器发送网络请求,获取必要的inde ...

最新文章

  1. gulp+browserSync自动刷新页面
  2. CUDA out of memory. Tried to allocate 392.00 MiB (GPU 0; 10.76 GiB total capacity; 652.77 MiB alread
  3. 介绍Azure服务平台,.NET Services及其中的访问控制服务(Access Control)
  4. javaWeb Note1
  5. [react] react中的setState和replaceState的区别是什么?
  6. 前端学习(2988):vue+element今日头条管理--使用技术栈
  7. 深度学习笔记(16) 误差分析(一)
  8. System.Data.OracleClient.OracleConnection的类型初始值设定项引发异常.
  9. [C#]使用Costura.Fody将源DLL合并到目标EXE
  10. redis tutorail
  11. 【APP 测试】绕过华为手机打开 USB 调试需要先登录华为账号问题
  12. 文件解密 [Java]
  13. 用Wireshark+小度WIFI抓手机app包
  14. 微信开发工具使用git
  15. 史上最猛“员工”,疯狂吐槽亿万富翁老板小扎:那么有钱,还总穿着同样的衣服!
  16. 小明一家过桥_智力题(小明一家过桥)
  17. Android学习之导航
  18. JAVA 数字图像处理----非白即黑的灰,2B青年的自画像
  19. 复习1:bool类型和char数组
  20. Arduino入门:按钮升级(按一下按钮,LED亮,再按一下,LED熄灭)

热门文章

  1. 操作系统2015(四川大学软件学院)
  2. Unity 2D血条制作方式
  3. 如何查看主机的网卡MAC地址及含义
  4. 计算机因特尔网络论文,[心得]英特尔
  5. 用了pcl的地方, 程序直接崩溃 挂掉
  6. httpqyl.php,linux运维架构--PHP开发-零基础学习PHP视频教程
  7. Babel转码器(ES6)
  8. 计算机时间转换工具,计算机时间的转换
  9. 新浪微博html5手机版,新浪微博手机版2018
  10. Debain 安装SVN服务器 支持http/https 全程指导