Khi xây dựng các ứng dụng web hiện đại, đảm bảo sự tương tác mượt mà cho người dùng là điều cực kỳ quan trọng. React, với khả năng cập nhật giao diện khai báo mạnh mẽ và kiến trúc dựa trên thành phần, mang đến sự linh hoạt tuyệt vời.
Tuy nhiên, khi ứng dụng phát triển phức tạp, hiệu suất có thể giảm sút, đặc biệt khi xử lý các bộ dữ liệu lớn hoặc tương tác người dùng nặng.
Bài viết này sẽ làm nổi bật những thử thách mà chúng tôi gặp phải khi triển khai bộ lọc tìm kiếm đơn giản trong React và cách chúng tôi khắc phục các vấn đề hiệu suất bằng cách tối ưu hóa thành phần.
Vấn Đề: Tương tác người dùng với dữ liệu lớn
Hãy tưởng tượng một ứng dụng hiển thị một danh sách lớn người dùng, và người dùng có thể lọc danh sách này bằng cách gõ vào ô tìm kiếm. Nhìn qua, đây có vẻ là một tác vụ đơn giản, nhưng khi số lượng người dùng tăng lên, ngay cả những sự thiếu sót nhỏ cũng có thể gây ra vấn đề hiệu suất nghiêm trọng.
Ban đầu, tôi xây dựng một thành phần bộ lọc tìm kiếm cho phép người dùng lọc danh sách 5.000 người dùng theo tên hoặc email. Tương tác bao gồm việc gõ vào trường nhập liệu và lọc động danh sách khi người dùng gõ. Tuy nhiên, hiệu suất nhanh chóng trở thành vấn đề.
Cách tiếp cận ban đầu
Đây là phiên bản đầu tiên của thành phần của tôi:
import React, { useState, useTransition } from "react"; // 🔧 Fake user generator
const generateUsers = (count: number) => { const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"]; return Array.from({ length: count }, (_, i) => ({ id: i, name: `${names[i % names.length]} ${i}`, email: `user${i}@example.com`, }));
}; const usersData = generateUsers(5000); export default function MassiveSearchFilter() { const [query, setQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(usersData); const [isPending, startTransition] = useTransition(); const handleSearch = (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); }); }; const highlight = (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"> 👩💻 Massive Search Filter (5,000 Users) </h1> <input type="text" value={query} onChange={handleSearch} placeholder="Search name or email..." 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"> 🔄 Loading results... </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> );
}
Những thử thách đã gặp phải
Lúc đầu, mọi thứ có vẻ ổn. Tìm kiếm nhanh và kết quả xuất hiện gần như ngay lập tức. Tuy nhiên, khi tôi bắt đầu thử nghiệm với các bộ dữ liệu lớn hơn (ví dụ, hơn 5.000 người dùng), các vấn đề sau đây nhanh chóng xuất hiện:
1. Tắc nghẽn hiệu suất
- Thành phần sẽ chậm đi đáng kể khi người dùng gõ nhanh trong ô tìm kiếm.
- React sẽ vẽ lại toàn bộ danh sách 5.000 người dùng mỗi khi người dùng gõ, gây ra độ trễ trong việc cập nhật giao diện.
2. Đóng băng giao diện người dùng
Vì bộ lọc tìm kiếm liên kết trực tiếp với trường nhập liệu và tương tác người dùng, việc gõ hoặc xóa văn bản nhanh chóng khiến giao diện người dùng bị đóng băng, vì React bị quá tải với việc lọc và vẽ lại hàng ngàn node DOM.
3. Quá phụ thuộc vào việc lọc
Mỗi thay đổi trong ô tìm kiếm đều kích hoạt một lần lọc lại toàn bộ danh sách, ngay cả khi người dùng vẫn đang gõ, điều này rất không hiệu quả.
Quy trình tối ưu hóa
Sau khi xác định các vấn đề, tôi đã áp dụng một số kỹ thuật để cải thiện hiệu suất và trải nghiệm người dùng:
Bước 1: Sử dụng react-window
để ảo hóa
Việc vẽ 5.000+ mục là một ví dụ điển hình cho ảo hóa. Tôi quyết định sử dụng react-window, cho phép React chỉ vẽ những mục đang hiển thị trong cửa sổ xem. Điều này có nghĩa là ngay cả với bộ dữ liệu lớn, tôi không cần vẽ toàn bộ danh sách, giảm đáng kể tải cho trình duyệt.
Bước 2: Ghi nhớ kết quả lọc
Bằng cách sử dụng hook useMemo
, tôi ghi nhớ kết quả lọc dựa trên truy vấn. Điều này đảm bảo rằng việc tính toán lọc chỉ xảy ra khi truy vấn thay đổi, thay vì mỗi lần vẽ lại.
Bước 3: Triển khai startTransition
để cập nhật giao diện mượt mà
Hook useTransition
cho phép tôi ưu tiên cập nhật trường nhập liệu trong khi hoãn việc tính toán lọc. Điều này đảm bảo rằng giao diện người dùng vẫn mượt mà trong khi ứng dụng thực hiện tác vụ tốn kém hơn là lọc dữ liệu.
Thành phần được tối ưu hóa
Sau đây là phiên bản tối ưu của thành phần sau những thay đổi này:
import React, { useState, useMemo, useTransition } from "react";
import { FixedSizeList as List } from "react-window"; const generateUsers = (count: number) => { const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"]; return Array.from({ length: count }, (_, i) => ({ id: i, name: `${names[i % names.length]} ${i}`, email: `user${i}@example.com`, }));
}; const usersData = generateUsers(5000); export default function MassiveSearchFilter() { 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]); const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; startTransition(() => setQuery(value)); }; const highlight = (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)} </> ); }; const Row = ({ 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"> 👩💻 Massive Search Filter (Virtualized) </h1> <input type="text" value={query} onChange={handleSearch} placeholder="Search name or email..." 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"> 🔄 Filtering... </div> )} {filteredUsers.length === 0 ? ( <div className="p-4 text-gray-500 italic text-center">No users found.</div> ) : ( <List height={500} itemCount={filteredUsers.length} itemSize={70} width="100%" className="border-t border-pink-100" > {Row} </List> )} </div> </div> );
}
Kết luận
Bằng cách áp dụng ảo hóa, ghi nhớ và các chiến lược ưu tiên giao diện người dùng, chúng tôi đã có thể cải thiện đáng kể hiệu suất của thành phần bộ lọc tìm kiếm. Tương tác người dùng hiện tại mượt mà và phản hồi nhanh, ngay cả khi phải lọc qua hàng ngàn mục.
Kinh nghiệm này làm nổi bật tầm quan trọng của việc tối ưu hóa hiệu suất khi xử lý dữ liệu lớn hoặc các tương tác người dùng phức tạp. Các hook tích hợp sẵn của React như useMemo
, useTransition
và các thư viện bên thứ ba như react-window
có thể tạo ra sự khác biệt lớn về mức độ hiệu suất và sự mượt mà của ứng dụng đối với người dùng.
Hy vọng các thông tin vừa rồi sẽ giúp ích cho các bạn!