虚拟列表组件如果滑动速度过快导致渲染性能问题
列举 antdesign
中的 虚拟列表组件(VirtualList
或 List
)
1. Ant Design 的 VirtualList 简介
Ant Design
提供的虚拟列表组件(如 VirtualList)基于虚拟滚动技术,适用于展示大量数据的场景。它通过只渲染可视区域的元素,大幅减少 DOM 节点数量
,提高性能。
常见问题
- 当用户快速滚动时,可能出现:
- 白屏问题:渲染逻辑跟不上滚动速度。
- 卡顿问题:滚动事件触发过于频繁,导致主线程阻塞。
解决方向
- 减少滚动触发频率:通过节流降低滚动事件的触发频率。
- 增加缓冲区:提前渲染可视区域上下的数据,避免白屏。
- 分片渲染:将渲染任务拆分成小块,避免主线程阻塞。
- 调整组件属性:优化 VirtualList 或 List 的配置。
2. 调整 Ant Design 虚拟列表的配置
Ant Design 的虚拟列表支持一些关键配置,以下是常用优化方法:
import React from 'react';
import { List } from 'antd';
import VirtualList from 'rc-virtual-list';const data = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);const MyVirtualList = () => {const itemHeight = 50; // 每项的固定高度const containerHeight = 500; // 容器高度return (<List><VirtualListdata={data}height={containerHeight} // 容器高度itemHeight={itemHeight} // 每项高度itemKey="id" // 唯一标识onScroll={(e) => {console.log('scrolling', e.currentTarget.scrollTop);}}>{(item) => <List.Item>{item}</List.Item>}</VirtualList></List>);
};export default MyVirtualList;
关键配置说明
- height: 设置容器的高度,确保虚拟滚动生效。
- itemHeight: 每个列表项的固定高度,虚拟列表性能依赖于此值。
- itemKey: 设置唯一标识,减少 DOM diff 的开销。
- onScroll: 监听滚动事件,用于调试或自定义滚动逻辑。
3. 针对快速滚动的优化策略
(1) 增加缓冲区(当滚动速度过快时,虚拟列表可能只渲染当前可视区域的内容,导致白屏)
-
解决方案
Ant Design 虚拟列表支持缓冲区(buffer)
,通过 VirtualList 的 height 和 itemHeight 动态调整缓冲区大小。<VirtualListdata={data}height={containerHeight} // 容器高度itemHeight={itemHeight} // 每项高度itemKey="id" // 唯一标识// 增加缓冲区onScroll={(e) => {console.log('scrolling', e.currentTarget.scrollTop);}} >{(item) => <List.Item>{item}</List.Item>} </VirtualList>
(2) 滚动事件的节流(滚动事件触发频率过高,导致主线程被占用)
-
解决方案
对滚动事件进行 节流 处理,降低触发频率import { throttle } from 'lodash';const handleScroll = throttle((e) => {console.log('Scrolled:', e.currentTarget.scrollTop); }, 16); // 每 16ms 触发一次<VirtualListdata={data}height={containerHeight}itemHeight={itemHeight}itemKey="id"onScroll={handleScroll} // 使用节流处理滚动事件 >{(item) => <List.Item>{item}</List.Item>} </VirtualList>
(3) 分片渲染(渲染任务太重,滚动时主线程被阻塞)
-
解决方案
将渲染任务拆分为小块,分批完成。import React, { useState, useEffect } from 'react'; import { List } from 'antd';const MyVirtualList = () => {const [renderedData, setRenderedData] = useState([]);const data = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);useEffect(() => {let start = 0;const chunkSize = 100; // 每次渲染 100 个const interval = setInterval(() => {if (start >= data.length) {clearInterval(interval); // 渲染完成return;}setRenderedData((prev) => [...prev, ...data.slice(start, start + chunkSize)]);start += chunkSize;}, 50); // 每 50ms 渲染一次}, []);return (<ListdataSource={renderedData}renderItem={(item) => <List.Item>{item}</List.Item>}/>); };export default MyVirtualList;
(4) 动态加载数据(一次性加载所有数据可能导致内存占用过高)
-
解决方案
使用分页或动态加载策略,只加载当前可视区域及其缓冲区的数据。const MyVirtualList = () => {const [data, setData] = useState([]);const [page, setPage] = useState(1);useEffect(() => {// 模拟异步加载数据const fetchData = async () => {const newData = Array.from({ length: 100 }, (_, i) => `Item ${i + (page - 1) * 100}`);setData((prev) => [...prev, ...newData]);};fetchData();}, [page]);const handleScroll = (e) => {const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;if (scrollTop + clientHeight >= scrollHeight - 50) {setPage((prev) => prev + 1); // 加载下一页}};return (<List><VirtualListdata={data}height={500}itemHeight={50}itemKey="id"onScroll={handleScroll}>{(item) => <List.Item>{item}</List.Item>}</VirtualList></List>); };
4. 使用 Intersection Observer
通过 Intersection Observer 动态检测哪些元素进入了可视区域,只在需要时渲染这些元素
import React, { useRef, useEffect } from 'react';const MyVirtualList = ({ items }) => {const observer = useRef();useEffect(() => {observer.current = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {const index = entry.target.dataset.index;entry.target.textContent = `Item ${index}`;}});});const elements = document.querySelectorAll('.list-item');elements.forEach((el) => observer.current.observe(el));return () => observer.current.disconnect();}, []);return (<div style={{ height: '500px', overflow: 'auto' }}>{items.map((_, index) => (<div key={index} data-index={index} className="list-item" style={{ height: '50px' }}>Loading...</div>))}</div>);
};export default MyVirtualList;