引言:在处理大规模数据集渲染时,前端性能常常面临巨大的挑战。本文将探讨
react-virtualized-list
库如何通过虚拟化技术和
「Intersection Observer API」
,实现前端渲染性能飙升
「50%」
的突破,页面渲染速度提升
「95%」
!借助目录阅读,效果更佳。
事情的起因
最近,公司监控系统出现了加载卡顿和白屏问题,需要一个能够处理大规模数据渲染的方案。由于核心需求是列表项数据需要动态更新和自动刷新,所以用到了 react-virtualized-list 库。
如果你正在寻找解决大数据集渲染瓶颈的方法,或是希望提升前端应用的响应速度,这篇文章将为你带来全新的启发与实用的解决方案。希望对你有所帮助、有所借鉴。大家有什么疑问或者建议,欢迎在评论区一起讨论!
什么是虚拟化?
虚拟化技术,顾名思义,是一种通过仅渲染当前用户可见的数据项,而不是整个数据集,来优化性能的技术。这种技术在处理大量数据时尤为重要,因为它显著减少了 DOM 节点的数量,从而提高了性能。通过虚拟化,可以在用户滚动列表时动态加载和卸载元素,保持界面流畅。
下面是
react-virtualized-list
在虚拟化方面做的处理:
我们来看看真实的 DOM 情况!
react-virtualized-list
简介
react-virtualized-list 是一个专门用于显示大型数据集的高性能 React 组件库。它同时适用于
「PC 端」
和
「移动端」
,通过虚拟化技术实现了延迟加载和无限滚动功能,尤其是非常适合需要高效渲染和加载大量数据的应用场景,如聊天记录、商品列表等。
此外,
react-virtualized-list
库还提供了场景适用的效果展示和示例代码。
核心特性 🔥🔥
-
「高性能」
:仅渲染当前视口内的元素,显著减少 DOM 节点数量。
-
「延迟加载」
:动态加载数据,避免一次性加载大量数据带来的性能问题。
-
「无限滚动」
:支持无限滚动,用户可以持续滚动查看更多内容。
-
「自定义渲染」
:提供灵活的 API,允许开发者自定义列表项的渲染方式。
-
「视口内刷新」
:支持自动刷新视口内的内容,确保数据的实时性。
-
「支持 TS 和 JS」
:适用于 TypeScript 和 JavaScript 项目。
安装
可以通过 npm 或 yarn 轻松安装 react-virtualized-list:
npm install react-virtualized-list
# 或者
yarn add react-virtualized-list
基本用法
下面是一个简单的示例,展示了如何使用
react-virtualized-list
创建一个无限滚动的虚拟化列表:
import React, { useState, useEffect } from 'react';
import VirtualizedList from 'react-virtualized-list';
import './style/common.css';
const InfiniteScrollList = () => {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const loadMoreItems = () => {
// 模拟 API 调用,延迟 1 秒加载新数据
setTimeout(() => {
const newItems = Array.from({ length: 20 }, (_, index) => ({
id: items.length + index,
text: `Item ${items.length + index}`
}));
setItems(prevItems => [...prevItems, ...newItems]);
setHasMore(newItems.length > 0);
}, 1000);
};
useEffect(() => {
loadMoreItems();
}, []);
const renderItem = (item) => <div>{item.text}div>;
return (
<div className='content'>
<VirtualizedList
listData={items}
renderItem={renderItem}
containerHeight='450px'
itemClassName='item-class'
onLoadMore={loadMoreItems}
hasMore={hasMore}
loader={<div>Loading...div>}
endMessage={<div>No more itemsdiv>}
/>
div>
);
};
export default InfiniteScrollList;
/* ./style/common.css */
.content {
width: 350px;
padding: 16px;
border: 1px solid red;
margin-top: 10vh;
}
.item-class {
height: 50px;
border: 1px solid blue;
margin: 0px 0 10px;
padding: 10px;
background-color: #f0f0f0;
}
通过
onLoadMore
和
hasMore
属性实现无限滚动,在用户滚动到列表底部时自动加载更多数据。这种功能常见于滚动加载下页数据。
进阶用法
1. 动态加载数据
为了进一步提高性能,可以使用动态加载技术,只在需要时加载数据。以下是一个示例,展示了如何结合
react-virtualized-list
和动态数据加载:
import React, { useState, useEffect } from 'react';
import VirtualizedList from 'react-virtualized-list';
import './style/common.css';
const fetchProductData = async (product) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ description: `Description for ${product.name}`, imageUrl: `https://via.placeholder.com/150?text=Product+${product.id}` });
}, 500);
});
};
const fetchProducts = async (page) => {
return new Promise((resolve) => {
setTimeout(() => {
const products = Array.from({ length: 10 }, (_, i) => ({
id: page * 10 + i,
name: `Product ${page * 10 + i}`
}));
resolve(products);
}, 500);
});
};
const DynamicInfiniteList = () => {
const [products, setProducts] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(0);
const loadMoreProducts = async () => {
const newProducts = await fetchProducts(page);
setProducts(prevProducts => [...prevProducts, ...newProducts]);
setPage(prevPage => prevPage + 1);
if (newProducts.length < 10) setHasMore(false);
};
useEffect(() => {
loadMoreProducts();
}, []);
return (
<div className='content'>
<VirtualizedList
listData={products}
renderItem={(product, data) => (
<div>
<h2>{product.name}h2>
<p>{data ? data.description : 'Loading...'}p>
{data && <img src={data.imageUrl} alt={product.name} />}
div>
)}
itemClassName='item-class-dynamic'
fetchItemData={fetchProductData}
onLoadMore={loadMoreProducts}
hasMore={hasMore}
containerHeight='500px'
loader='Loading more products...'
endMessage='No more products'
/>
div>
);
};
export default DynamicInfiniteList;
/* ./style/common.css */
.content {
width: 350px;
padding: 16px;
border: 1px solid red;
margin-top: 10vh;
}
.item-class-dynamic {
height: 300px;
padding: 20px;
border-bottom: 1px solid #eee;
}
「注意」
:在上面代码中,我们使用
onLoadMore
模拟商品列表的滚动加载,并在
VirtualizedList
组件的
fetchItemData
实现了商品详情的动态加载。这对于大数据集下,
「后端无法一次性返回数据非常有利」
!
2. 自定义渲染
react-virtualized-list
还提供了自定义渲染功能,开发者可以根据具体需求定制列表项的渲染方式。以下是一个示例,展示了如何自定义列表项的样式和内容:
import React from 'react';
import VirtualizedList from 'react-virtualized-list';
const data = Array.from({ length: 1000 }).map((_, index) => ({
title: `Item ${index}`,
index: index,
description: `This is the description for item ${index}.`
}));
const ListItem = ({ item, style }) => (
<div style={{ ...style, padding: '10px', borderBottom: '1px solid #ccc' }}>
<h3>{item.title}h3>
<p>{item.description}p>
div>
);
const itemStyle = {
height: '100px',
border: '1px solid blue',
margin: '0px 0 10px',
padding: '10px',
backgroundColor: '#f0f0f0'
};
const MyVirtualizedList = () => (
<div style={{width: '350px', padding: '16px', border: '1px solid red'}}>
<VirtualizedList
listData={data}
itemStyle={itemStyle}
renderItem={({ index, style }) => <ListItem item={data[index]} style={style} />}
containerHeight='80vh'
/>
div>
);
export default MyVirtualizedList;
此外,
react-virtualized-list
还提供了其他的用法场景和相关 API,详情请见使用文档。
实现原理(🔥核心重点)
在构建大型 Web 应用时,经常会遇到需要展示大量数据的情况,比如电子商务平台的产品列表等。传统的渲染方式可能会面临性能问题,因为它们需要在页面上同时呈现大量 DOM 元素,导致页面加载缓慢、滚动卡顿等问题。
为了解决这个问题,我们可以使用虚拟化列表来优化渲染过程。而
react-virtualized-list
库的核心在于通过虚拟化技术优化渲染过程。其主要原理包括以下几点:
1. 可视区域监测:利用Intersection Observer API
在虚拟化列表的实现中,一个关键步骤是监测可视区域内的元素。为了实现懒加载、滚动动画等需求,传统的方法是通过监听滚动事件并计算每个元素的位置来实现。需要获取
「元素与视窗的交叉状态」
,这通常使用
「监听滚动事件 + 计算偏移量 + 判断逻辑」
的方式实现,再配合防抖等优化,然而这种方式效率较低。
// 获取需要监测可视性的元素
const elements = document.querySelectorAll('.target-element');
// 监听滚动事件
window.addEventListener('scroll', () => {
// 计算每个元素的位置
elements.forEach(element => {
const rect = element.getBoundingClientRect();
if (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
) {
// 元素在可视区域内
// 执行相应操作
console.log(`${element} is visible.`);
}
});
});
相比之下,我们可以利用现代浏览器提供的 Intersection Observer API 来更高效地监测元素的可见性变化。
// 定义一个 Intersection Observer
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// 如果元素可见
if (entry.isIntersecting) {
// 执行相应操作
console.log(`${entry.target} is visible.`);
}
});
});
// 获取需要监测可视性的元素
const elements = document.querySelectorAll('.target-element');
// 监测每个元素
elements.forEach(element => {
observer.observe(element);
});
这里封装了一个 React Hooks
useIntersectionObserver
,提供了
Intersection Observer API
的能力。
「2. 仅渲染可见区域:优化性能」
虚拟化列表的另一个关键优化是仅渲染可见区域内的元素,而不是渲染整个列表。这样做可以大大减少渲染所需的时间和资源,提高页面的性能表现。
import useIntersectionObserver from './useIntersectionObserver';
const [visibleItems, setVisibleItems] = useState<Set>(new Set());
const handleVisibilityChange = useCallback((isVisible: boolean, entry: IntersectionObserverEntry) => {
const index = parseInt(entry.target.getAttribute('data-index')!, 10);
setVisibleItems(prev => {
const newVisibleItems = new Set(prev);
if (isVisible) {
newVisibleItems.add(index);
} else {
newVisibleItems.delete(index);
}
return newVisibleItems;
});
}, []);
const { observe, unobserve } = useIntersectionObserver(containerRef.current, handleVisibilityChange, null, observerOptions);
3. 动态加载和卸载:保持内存使用最小化
最后,虚拟化列表还可以通过动态加载和卸载元素来保持内存使用最小化。当用户滚动到可视区域时,新的元素被动态加载,而离开可视区域的元素则被卸载,从而减少页面的内存占用。
const visibleRange = useMemo(() => {
const sortedVisibleItems = [...visibleItems].sort((a, b) => a - b);
const firstVisible = sortedVisibleItems[0] || 0;
const lastVisible = sortedVisibleItems[sortedVisibleItems.length - 1] || 0;
// 设置缓存区
return [Math.max(0, firstVisible - BUFFER_SIZE), Math.min(listData.length - 1, lastVisible + BUFFER_SIZE)];
}, [visibleItems, listData.length]);
const renderItems = () => {
return listData.length ? listData.map((item, index) => {
if (index >= visibleRange[0] && index <= visibleRange[1]) {
return (
<div
className={itemClassName || undefined}
style={itemContainerStyle}
ref={node => handleRef(node, index)}
key={index}
data-index={index}
>
<VirtualizedListItem
item={listData[index]}
isVisible={visibleItems.has(index)}
refreshOnVisible={refreshOnVisible}
fetchItemData={fetchItemData}
itemLoader={itemLoader}
>
{renderItem}
VirtualizedListItem>
div>
);
}
return null;
}) : (
emptyListMessage ? emptyListMessage : null
);
};
当元素进入视口时,我们加载它;当元素离开视口时,我们卸载它。这样就可以保持页面上始终只有视口内的内容被渲染,从而提高页面的性能和响应速度。
❝
除此之外,通过使用
useMemo
计算当前可见的列表项范围 (
visibleRange
),以及设置一个缓冲区 (
BUFFER_SIZE
);使用
useMemo
和
useCallback
用于性能优化的 Hook。它们帮助避免不必要的计算和重新渲染。
❞
性能对比(🔥性能飙升 50%)
下面我们就来看下,传统滚动 Scroll 监听和 Intersection Observer API 的性能对比数据(假设在相同环境和数据集下测试):
方法
|
初始渲染时间
|
滚动性能
|
内存使用
|
传统滚动监听
|
300ms
|
低
|
高
|
Intersection Observer API
|
150ms
|
高
|
低
|
-
「初始渲染时间」
:使用 Intersection Observer API 的初始渲染时间较短,因为只渲染可见区域。
-
「滚动性能」
:传统滚动监听由于频繁的滚动事件触发和位置计算,滚动性能较低;Intersection Observer API 的滚动性能较高,因为它利用了浏览器的优化机制。
-
「内存使用」
:Intersection Observer API 由于仅加载和渲染可见元素,内存使用更低。
性能测试代码分析
以下是一个示例,展示了如何使用 console.time 和 console.timeEnd 来测量性能:
// 测量传统滚动监听的性能
console.time('Scroll');
window.addEventListener('scroll', () => {
// 模拟计算每个元素的位置
const elements = document.querySelectorAll('.target-element');
elements.forEach(element => {
const rect = element.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
// 模拟渲染逻辑
}
});
});
console.timeEnd('Scroll');
// 测量 Intersection Observer API 的性能
console.time('IntersectionObserver');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 模拟渲染逻辑
}
});
});
const elements = document.querySelectorAll('.target-element');
elements.forEach(element => observer.observe(element));
console.timeEnd('IntersectionObserver');
❝
「注意」
:传统滚动监听方法还会涉及大量计算,这里仅简单测量了监听性能的统计部分。
❞
传统的滚动监听方式通过监听
scroll
事件,在每次滚动时计算每个目标元素的位置,并判断其是否在视窗内。这部分代码的执行会阻塞主线程,尤其在滚动频繁的情况下可能导致性能问题,因为需要不断重新计算元素位置。
相比之下,Intersection Observer API 更高效。它可以检测元素是否可见,并在元素进入或退出视窗时触发回调函数,从而实现需要的功能。