React 的 useTransition:構(gòu)建高性能搜索的五萬(wàn)條記錄案例研究
在構(gòu)建現(xiàn)代 Web 應(yīng)用時(shí),確保流暢的用戶(hù)交互至關(guān)重要。React 憑借其強(qiáng)大的聲明式 UI 更新和基于組件的架構(gòu),提供了極大的靈活性。然而,隨著應(yīng)用復(fù)雜度的增加,性能可能會(huì)下降,尤其是在處理大數(shù)據(jù)集或密集用戶(hù)交互時(shí)。本文將重點(diǎn)介紹我們?cè)?React 中實(shí)現(xiàn)簡(jiǎn)單搜索過(guò)濾器時(shí)遇到的挑戰(zhàn),以及如何通過(guò)優(yōu)化組件來(lái)克服性能問(wèn)題。
問(wèn)題:與大數(shù)據(jù)集的用戶(hù)交互
想象一個(gè)應(yīng)用,它顯示一個(gè)龐大的用戶(hù)列表,用戶(hù)可以通過(guò)在搜索框中輸入來(lái)過(guò)濾該列表。乍一看,這似乎是一個(gè)簡(jiǎn)單的任務(wù),但隨著用戶(hù)數(shù)量的增加,即使是很小的低效也會(huì)導(dǎo)致嚴(yán)重的性能問(wèn)題。我們最初構(gòu)建了一個(gè)搜索過(guò)濾器組件,允許用戶(hù)通過(guò)姓名或電子郵件過(guò)濾 5,000 名用戶(hù)的列表。交互包括在輸入字段中輸入,并在用戶(hù)輸入時(shí)動(dòng)態(tài)過(guò)濾列表。然而,性能很快就變得有問(wèn)題。
原始方法
這是我們組件的第一個(gè)版本:
import React, { useState, useTransition } from"react";
// ?? 假用戶(hù)生成器
constgenerateUsers = (count: number) => {
const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"];
returnArray.from({ length: count }, (_, i) => ({
id: i,
name: `${names[i % names.length]} ${i}`,
email: `user${i}@example.com`,
}));
};
const usersData = generateUsers(5000);
exportdefaultfunctionMassiveSearchFilter() {
const [query, setQuery] = useState("");
const [filteredUsers, setFilteredUsers] = useState(usersData);
const [isPending, startTransition] = useTransition();
consthandleSearch = (e: { target: { value: any; }; }) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
const lower = value.toLowerCase();
const filtered = usersData.filter(
(user) =>
user.name.toLowerCase().includes(lower) ||
user.email.toLowerCase().includes(lower)
);
setFilteredUsers(filtered);
});
};
consthighlight = (text: string, query: string) => {
if (!query) return text;
const index = text.toLowerCase().indexOf(query.toLowerCase());
if (index === -1) return text;
return (
<>
{text.slice(0, index)}
<mark className="bg-pink-200 text-black">
{text.slice(index, index + query.length)}
</mark>
{text.slice(index + query.length)}
</>
);
};
return (
<div className="min-h-screen bg-gray-100 p-6 font-sans">
<div className="max-w-3xl mx-auto bg-white p-6 rounded-2xl border-2 border-pink-200 shadow-md">
<h1 className="text-2xl font-bold text-pink-600 mb-4">
???? 大規(guī)模搜索過(guò)濾器(5,000 用戶(hù))
</h1>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="搜索姓名或電子郵件..."
className="w-full p-3 mb-4 border-2 border-pink-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
{isPending && (
<div className="text-pink-500 italic mb-2 text-sm animate-pulse">
?? 加載結(jié)果中...
</div>
)}
<div className="max-h-[500px] overflow-y-auto divide-y divide-pink-100 border-t border-pink-100">
{filteredUsers.map((user) => (
<div
key={user.id}
className="p-3 hover:bg-pink-50 transition-colors duration-150"
>
<p className="font-medium text-gray-800 text-base">
{highlight(user.name, query)}
</p>
<p className="text-sm text-gray-500">
{highlight(user.email, query)}
</p>
</div>
))}
</div>
</div>
</div>
);
}
面臨的挑戰(zhàn)
起初,一切似乎都很順利。搜索速度很快,結(jié)果幾乎瞬間出現(xiàn)。然而,當(dāng)我們開(kāi)始使用更大的數(shù)據(jù)集(例如 5,000 多名用戶(hù))進(jìn)行測(cè)試時(shí),以下問(wèn)題很快就變得明顯:
- 性能瓶頸:在搜索字段中快速輸入時(shí),組件會(huì)顯著變慢。React 會(huì)在每次按鍵時(shí)重新渲染整個(gè) 5,000 名用戶(hù)的列表,導(dǎo)致 UI 更新延遲。
- UI 凍結(jié):由于搜索過(guò)濾器直接與輸入字段和用戶(hù)交互綁定,快速輸入或刪除文本會(huì)導(dǎo)致 UI 凍結(jié),因?yàn)?React 被過(guò)濾和重新渲染數(shù)千個(gè) DOM 節(jié)點(diǎn)的任務(wù)壓垮了。
- 過(guò)度依賴(lài)過(guò)濾:搜索輸入中的每次更改都會(huì)觸發(fā)對(duì)整個(gè)列表的過(guò)濾,即使用戶(hù)仍在輸入,這也是低效的。
優(yōu)化過(guò)程
在識(shí)別出問(wèn)題后,我們應(yīng)用了幾種技術(shù)來(lái)提高性能和用戶(hù)體驗(yàn):
步驟 1:使用 react-window 進(jìn)行虛擬化
渲染 5,000 多個(gè)項(xiàng)目是虛擬化的經(jīng)典案例。我們決定使用 react-window,它允許 React 僅渲染視口中可見(jiàn)的項(xiàng)目。這意味著即使在大數(shù)據(jù)集的情況下,我們也不需要渲染整個(gè)列表,從而大大減少了瀏覽器的負(fù)載。
步驟 2:過(guò)濾結(jié)果的記憶化
通過(guò)使用 useMemo 鉤子,我們根據(jù)查詢(xún)對(duì)過(guò)濾結(jié)果進(jìn)行了記憶化。這確保了過(guò)濾計(jì)算僅在查詢(xún)更改時(shí)發(fā)生,而不是在每次渲染時(shí)發(fā)生。
步驟 3:使用 startTransition 實(shí)現(xiàn)平滑的 UI 更新
useTransition 鉤子允許我們優(yōu)先更新輸入字段,同時(shí)推遲過(guò)濾計(jì)算。這確保了 UI 在應(yīng)用執(zhí)行更昂貴的過(guò)濾任務(wù)時(shí)仍保持響應(yīng)。
優(yōu)化后的組件
以下是經(jīng)過(guò)這些更改后的優(yōu)化版本組件:
import React, { useState, useMemo, useTransition } from"react";
import { FixedSizeListasList } from"react-window";
constgenerateUsers = (count: number) => {
const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"];
returnArray.from({ length: count }, (_, i) => ({
id: i,
name: `${names[i % names.length]} ${i}`,
email: `user${i}@example.com`,
}));
};
const usersData = generateUsers(5000);
exportdefaultfunctionMassiveSearchFilter() {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const filteredUsers = useMemo(() => {
const lower = query.toLowerCase();
return usersData.filter(
(user) =>
user.name.toLowerCase().includes(lower) ||
user.email.toLowerCase().includes(lower)
);
}, [query]);
consthandleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
startTransition(() =>setQuery(value));
};
consthighlight = (text: string, query: string) => {
if (!query) return text;
const index = text.toLowerCase().indexOf(query.toLowerCase());
if (index === -1) return text;
return (
<>
{text.slice(0, index)}
<mark className="bg-pink-200 text-black">
{text.slice(index, index + query.length)}
</mark>
{text.slice(index + query.length)}
</>
);
};
constRow = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const user = filteredUsers[index];
return (
<div
style={style}
key={user.id}
className="p-3 border-b border-pink-100 hover:bg-pink-50"
>
<p className="font-medium text-gray-800 text-base">
{highlight(user.name, query)}
</p>
<p className="text-sm text-gray-500">{highlight(user.email, query)}</p>
</div>
);
};
return (
<div className="min-h-screen bg-gray-100 p-6 font-sans">
<div className="max-w-3xl mx-auto bg-white p-6 rounded-2xl border-2 border-pink-200 shadow-md">
<h1 className="text-2xl font-bold text-pink-600 mb-4">
???? 大規(guī)模搜索過(guò)濾器(虛擬化)
</h1>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="搜索姓名或電子郵件..."
className="w-full p-3 mb-4 border-2 border-pink-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
{isPending && (
<div className="text-pink-500 italic mb-2 text-sm animate-pulse">
?? 過(guò)濾中...
</div>
)}
{filteredUsers.length === 0 ? (
<div className="p-4 text-gray-500 italic text-center">未找到用戶(hù)。</div>
) : (
<List
height={500}
itemCount={filteredUsers.length}
itemSize={70}
width="100%"
className="border-t border-pink-100"
>
{Row}
</List>
)}
</div>
</div>
);
}
結(jié)論
通過(guò)應(yīng)用虛擬化、記憶化和UI 優(yōu)先級(jí)策略,我們能夠顯著提高搜索過(guò)濾器組件的性能。用戶(hù)交互現(xiàn)在變得流暢且響應(yīng)迅速,即使有數(shù)千個(gè)項(xiàng)目需要過(guò)濾。
這一經(jīng)驗(yàn)凸顯了在處理大數(shù)據(jù)集或復(fù)雜用戶(hù)交互時(shí)優(yōu)化性能的重要性。React 的內(nèi)置鉤子如 useMemo、useTransition 以及第三方庫(kù)如 react-window 可以極大地影響應(yīng)用的用戶(hù)體驗(yàn),使其感覺(jué)更加流暢和高效。
代碼參考: https://github.com/paghar/react19-next15-trickysample
原文地址: https://dev.to/fpaghar/mastering-usetransition-in-react-building-a-high-performance-search-for-50k-record-case-study-1bdn作者: Fatemeh Paghar