引言:在處理大規(guī)模數(shù)據(jù)集渲染時(shí),前端性能常常面臨巨大的挑戰(zhàn)。本文將探討 react-virtualized-list
庫如何通過虛擬化技術(shù)和 Intersection Observer,實(shí)現(xiàn)前端渲染性能飆升 50% 的突破,頁面渲染速度提升 95% !????
背景
最近,公司監(jiān)控系統(tǒng)出現(xiàn)了加載卡頓和白屏問題,需要一個(gè)能夠處理大規(guī)模數(shù)據(jù)渲染的方案。由于核心需求是列表項(xiàng)數(shù)據(jù)需要動態(tài)更新和自動刷新,所以用到了 react-virtualized-list 庫。這個(gè)過程相當(dāng)曲折,具體業(yè)務(wù)需求細(xì)節(jié)后面我會詳細(xì)寫一篇文章,這里先介紹一下react-virtualized-list
庫的特性、適用場景、API和實(shí)現(xiàn)原理。
希望對你有所幫助、有所借鑒。大家有什么疑問或者建議,歡迎在評論區(qū)一起討論!
什么是虛擬化?
虛擬化技術(shù),顧名思義,是一種通過僅渲染當(dāng)前用戶可見的數(shù)據(jù)項(xiàng),而不是整個(gè)數(shù)據(jù)集,來優(yōu)化性能的技術(shù)。這種技術(shù)在處理大量數(shù)據(jù)時(shí)尤為重要,因?yàn)樗@著減少了 DOM 節(jié)點(diǎn)的數(shù)量,從而提高了性能。通過虛擬化,可以在用戶滾動列表時(shí)動態(tài)加載和卸載元素,保持界面流暢。
下面是react-virtualized-list
在虛擬化方面做的處理:
我們來看看真實(shí)的 DOM 情況!
react-virtualized-list
簡介
react-virtualized-list 是一個(gè)專門用于顯示大型數(shù)據(jù)集的高性能 React 組件庫。它同時(shí)適用于 PC 端和移動端,通過虛擬化技術(shù)實(shí)現(xiàn)了延遲加載和無限滾動功能,尤其是非常適合需要高效渲染和加載大量數(shù)據(jù)的應(yīng)用場景,如聊天記錄、商品列表等。
此外,react-virtualized-list
庫還提供了場景適用的效果展示和示例代碼。
核心特性 ????
- 高性能:僅渲染當(dāng)前視口內(nèi)的元素,顯著減少 DOM 節(jié)點(diǎn)數(shù)量。
- 延遲加載:動態(tài)加載數(shù)據(jù),避免一次性加載大量數(shù)據(jù)帶來的性能問題。
- 無限滾動:支持無限滾動,用戶可以持續(xù)滾動查看更多內(nèi)容。
- 自定義渲染:提供靈活的 API,允許開發(fā)者自定義列表項(xiàng)的渲染方式。
- 視口內(nèi)刷新:支持自動刷新視口內(nèi)的內(nèi)容,確保數(shù)據(jù)的實(shí)時(shí)性。
- 支持 TS 和 JS:適用于 TypeScript 和 JavaScript 項(xiàng)目。
安裝
可以通過 npm 或 yarn 輕松安裝 react-virtualized-list:
npm install react-virtualized-list
# 或者
yarn add react-virtualized-list
基本用法
下面是一個(gè)簡單的示例,展示了如何使用 react-virtualized-list
創(chuàng)建一個(gè)無限滾動的虛擬化列表:
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 調(diào)用,延遲 1 秒加載新數(shù)據(jù)
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 items</div>}
/>
</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
屬性實(shí)現(xiàn)無限滾動,在用戶滾動到列表底部時(shí)自動加載更多數(shù)據(jù)。這種功能常見于滾動加載下頁數(shù)據(jù)。
進(jìn)階用法
動態(tài)加載數(shù)據(jù)
為了進(jìn)一步提高性能,可以使用動態(tài)加載技術(shù),只在需要時(shí)加載數(shù)據(jù)。以下是一個(gè)示例,展示了如何結(jié)合 react-virtualized-list
和動態(tài)數(shù)據(jù)加載:
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
實(shí)現(xiàn)了商品詳情的動態(tài)加載。這對于大數(shù)據(jù)集下,后端無法一次性返回?cái)?shù)據(jù)非常有利!
自定義渲染
react-virtualized-list
還提供了自定義渲染功能,開發(fā)者可以根據(jù)具體需求定制列表項(xiàng)的渲染方式。以下是一個(gè)示例,展示了如何自定義列表項(xiàng)的樣式和內(nèi)容:
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
還提供了其他的用法場景和相關(guān) API,詳情請見使用文檔。
實(shí)現(xiàn)原理(??核心重點(diǎn),一定要了解)
在構(gòu)建大型 Web 應(yīng)用時(shí),經(jīng)常會遇到需要展示大量數(shù)據(jù)的情況,比如電子商務(wù)平臺的產(chǎn)品列表等。傳統(tǒng)的渲染方式可能會面臨性能問題,因?yàn)樗鼈冃枰陧撁嫔贤瑫r(shí)呈現(xiàn)大量 DOM 元素,導(dǎo)致頁面加載緩慢、滾動卡頓等問題。
為了解決這個(gè)問題,我們可以使用虛擬化列表來優(yōu)化渲染過程。而 react-virtualized-list
庫的核心在于通過虛擬化技術(shù)優(yōu)化渲染過程。其主要原理包括以下幾點(diǎn):
1. 可視區(qū)域監(jiān)測:利用Intersection Observer API
在虛擬化列表的實(shí)現(xiàn)中,一個(gè)關(guān)鍵步驟是監(jiān)測可視區(qū)域內(nèi)的元素。傳統(tǒng)的方法是通過監(jiān)聽滾動事件并計(jì)算每個(gè)元素的位置來實(shí)現(xiàn),然而這種方式效率較低。
// 獲取需要監(jiān)測可視性的元素
const elements = document.querySelectorAll('.target-element');
// 監(jiān)聽滾動事件
window.addEventListener('scroll', () => {
// 計(jì)算每個(gè)元素的位置
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)
) {
// 元素在可視區(qū)域內(nèi)
// 執(zhí)行相應(yīng)操作
console.log(`${element} is visible.`);
}
});
});
相比之下,我們可以利用現(xiàn)代瀏覽器提供的 Intersection Observer API 來更高效地監(jiān)測元素的可見性變化。
// 定義一個(gè) Intersection Observer
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// 如果元素可見
if (entry.isIntersecting) {
// 執(zhí)行相應(yīng)操作
console.log(`${entry.target} is visible.`);
}
});
});
// 獲取需要監(jiān)測可視性的元素
const elements = document.querySelectorAll('.target-element');
// 監(jiān)測每個(gè)元素
elements.forEach(element => {
observer.observe(element);
});
這里我封裝了一個(gè) React Hooks useIntersectionObserver
,提供了Intersection Observer API
的能力。
2. 僅渲染可見區(qū)域:優(yōu)化性能
虛擬化列表的另一個(gè)關(guān)鍵優(yōu)化是僅渲染可見區(qū)域內(nèi)的元素,而不是渲染整個(gè)列表。這樣做可以大大減少渲染所需的時(shí)間和資源,提高頁面的性能表現(xiàn)。
import useIntersectionObserver from './useIntersectionObserver';
const [visibleItems, setVisibleItems] = useState<Set<number>>(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. 動態(tài)加載和卸載:保持內(nèi)存使用最小化
最后,虛擬化列表還可以通過動態(tài)加載和卸載元素來保持內(nèi)存使用最小化。當(dāng)用戶滾動到可視區(qū)域時(shí),新的元素被動態(tài)加載,而離開可視區(qū)域的元素則被卸載,從而減少頁面的內(nèi)存占用。
const visibleRange = useMemo(() => {
const sortedVisibleItems = [...visibleItems].sort((a, b) => a - b);
const firstVisible = sortedVisibleItems[0] || 0;
const lastVisible = sortedVisibleItems[sortedVisibleItems.length - 1] || 0;
// 設(shè)置緩存區(qū)
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
);
};
當(dāng)元素進(jìn)入視口時(shí),我們加載它;當(dāng)元素離開視口時(shí),我們卸載它。這樣就可以保持頁面上始終只有視口內(nèi)的內(nèi)容被渲染,從而提高頁面的性能和響應(yīng)速度。
除此之外,通過使用
useMemo
計(jì)算當(dāng)前可見的列表項(xiàng)范圍 (visibleRange
),以及設(shè)置一個(gè)緩沖區(qū) (BUFFER_SIZE
);使用useMemo
和useCallback
用于性能優(yōu)化的 Hook。它們幫助避免不必要的計(jì)算和重新渲染。
性能對比(??性能飆升 50%)
下面我們就來看下,傳統(tǒng)滾動 Scroll 監(jiān)聽和 Intersection Observer API 的性能對比數(shù)據(jù)(假設(shè)在相同環(huán)境和數(shù)據(jù)集下測試):
方法 | 初始渲染時(shí)間 | 滾動性能 | 內(nèi)存使用 |
---|---|---|---|
傳統(tǒng)滾動監(jiān)聽 | 300ms | 低 | 高 |
Intersection Observer API | 150ms | 高 | 低 |
- 初始渲染時(shí)間:使用 Intersection Observer API 的初始渲染時(shí)間較短,因?yàn)橹讳秩究梢妳^(qū)域。
- 滾動性能:傳統(tǒng)滾動監(jiān)聽由于頻繁的滾動事件觸發(fā)和位置計(jì)算,滾動性能較低;Intersection Observer API 的滾動性能較高,因?yàn)樗昧藶g覽器的優(yōu)化機(jī)制。
- 內(nèi)存使用:Intersection Observer API 由于僅加載和渲染可見元素,內(nèi)存使用更低。
性能測試代碼分析
以下是一個(gè)示例,展示了如何使用 console.time 和 console.timeEnd 來測量性能:
// 測量傳統(tǒng)滾動監(jiān)聽的性能
console.time('Scroll');
window.addEventListener('scroll', () => {
// 模擬計(jì)算每個(gè)元素的位置
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');
注意:傳統(tǒng)滾動監(jiān)聽方法還會涉及大量計(jì)算,這里僅簡單測量了監(jiān)聽性能的統(tǒng)計(jì)部分。
傳統(tǒng)的滾動監(jiān)聽方式通過監(jiān)聽 scroll
事件,在每次滾動時(shí)計(jì)算每個(gè)目標(biāo)元素的位置,并判斷其是否在視窗內(nèi)。這部分代碼的執(zhí)行會阻塞主線程,尤其在滾動頻繁的情況下可能導(dǎo)致性能問題,因?yàn)樾枰粩嘀匦掠?jì)算元素位置。
相比之下,Intersection Observer API 更高效。它可以檢測元素是否可見,并在元素進(jìn)入或退出視窗時(shí)觸發(fā)回調(diào)函數(shù),從而實(shí)現(xiàn)需要的功能。
性能總結(jié)
在性能方面,傳統(tǒng)實(shí)現(xiàn)方法通常需要通過監(jiān)聽滾動(scroll)事件來計(jì)算元素位置。這種方法存在以下問題:
- 性能消耗大:頻繁監(jiān)聽滾動事件會導(dǎo)致性能消耗增加,尤其是在大型數(shù)據(jù)集的情況下。
- 計(jì)算復(fù)雜度高:需要手動計(jì)算每個(gè)列表項(xiàng)與視口的交叉情況,邏輯復(fù)雜且容易出錯。需要花費(fèi)大量時(shí)間和精力來優(yōu)化和調(diào)試這些計(jì)算邏輯。
相比之下,Intersection Observer API 的性能更優(yōu),具有以下優(yōu)點(diǎn):
-
性能開銷低:
Intersection Observer API
利用瀏覽器的內(nèi)部優(yōu)化機(jī)制,減少了不必要的計(jì)算和事件觸發(fā),從而提高了性能。相比之下,傳統(tǒng)的scroll
事件監(jiān)聽方式由于密集觸發(fā),可能會導(dǎo)致較大的性能問題。 -
多元素監(jiān)測:
Intersection Observer API
允許同時(shí)監(jiān)測多個(gè)元素的交叉狀態(tài),而不需要為每個(gè)元素都綁定事件監(jiān)聽器。這使得在處理復(fù)雜布局和交互時(shí)更加高效。 -
異步執(zhí)行:當(dāng)元素進(jìn)入或離開交叉狀態(tài)時(shí),
Intersection Observer
會異步執(zhí)行回調(diào)函數(shù),不會阻塞主線程。這有助于保持頁面的響應(yīng)性和流暢性。 -
應(yīng)用場景廣泛:
Intersection Observer API
可以應(yīng)用于多種場景,如懶加載、無限滾動、廣告展示與統(tǒng)計(jì)、頁面元素動畫等。這些應(yīng)用場景通常需要高效地處理元素與視口之間的交互。
綜上所述,Intersection Observer API
在處理大型數(shù)據(jù)集和復(fù)雜交互時(shí),相比傳統(tǒng)的 scroll
事件監(jiān)聽方式,提供了更高的性能和更靈活的解決方案。
項(xiàng)目成果展示(??渲染速度提升95%)
下面我們看下優(yōu)化后的性能,展示實(shí)際改進(jìn)的用戶體驗(yàn)和加載時(shí)間。
首先從視覺感官上看,幾乎是一瞬間圖表就加載了出來。我們接著再來看看接口Network與數(shù)據(jù)對比!
為了清楚地展示優(yōu)化前后頁面加載速度的提升,我們可以將相關(guān)數(shù)據(jù)整理成一個(gè)表格形式,如下所示:
優(yōu)化指標(biāo) | 優(yōu)化前 | 優(yōu)化后 | 加載速度提升 |
---|---|---|---|
總耗時(shí) | 15000 毫秒(15秒) | 750 毫秒 | 提速了95% |
這個(gè)表格展示了優(yōu)化措施的顯著效果,從中可以看出,經(jīng)過優(yōu)化后,整體加載時(shí)間也從15000毫秒大幅減少至750毫秒,加載速度提高了95%。
總結(jié)
通過使用 react-virtualized-list
庫,監(jiān)控系統(tǒng)項(xiàng)目前端渲染性能得到了顯著提升。統(tǒng)計(jì)結(jié)果顯示:頁面加載速度提高了 95%,用戶體驗(yàn)得到了明顯改善。如果你也在處理大數(shù)據(jù)集的渲染問題,不妨試試這個(gè)庫。
希望本文能對你有所幫助,有所借鑒!大家有什么疑問或者建議,歡迎在評論區(qū)一起討論。