Trong các ứng dụng React hiện đại, tốc độ tải dữ liệu và hiệu suất hiển thị đóng vai trò quan trọng trong việc nâng cao trải nghiệm người dùng. Một ứng dụng chậm chạp, mất nhiều thời gian để hiển thị nội dung có thể khiến người dùng rời bỏ ngay lập tức. Vì vậy, việc tối ưu hóa cách truy vấn và xử lý dữ liệu là điều cần thiết.

Bài viết này sẽ giới thiệu 5 kỹ thuật truy vấn dữ liệu nâng cao trong React giúp cải thiện hiệu suất ứng dụng, tăng tốc độ tải trang và mang lại trải nghiệm mượt mà hơn cho người dùng.

1.Yêu cầu dữ liệu song song

Yêu cầu dữ liệu song song liên quan đến việc truy vấn nhiều nguồn dữ liệu đồng thời thay vì tuần tự. Kỹ thuật này đặc biệt hữu ích khi ứng dụng của bạn cần lấy dữ liệu từ các nguồn hoặc API độc lập và không có yêu cầu nào phụ thuộc vào kết quả của yêu cầu khác. Bằng cách thực hiện các yêu cầu này song song, bạn có thể giảm đáng kể thời gian tải tổng thể của ứng dụng.

Hãy tưởng tượng chúng ta có một trang như sau. Phần giới thiệu (About) và phần bạn bè (Friends) cần lấy dữ liệu riêng biệt từ /users/<id>/users/<id>/friends. Nếu chúng ta thực hiện yêu cầu dữ liệu trong mỗi component, điều này sẽ gây ra vấn đề "waterfall request" (tức là yêu cầu dữ liệu phải thực hiện tuần tự, dẫn đến thời gian tải lâu hơn).

const Profile = ({ id }: { id: string }) => {
const [user, setUser] = useState<User | undefined>();

useEffect(() => {
const fetchUser = async () => {
const data = await get<User>(`/users/${id}`);
setUser(data);
};
fetchUser();
}, [id]);

return (
<>
{user && <About user={user} />}
<Friends id={id} />
</>
);
};

Ở trên, chúng ta lấy dữ liệu cho thông tin cơ bản của người dùng, và trong phần Friends:

const Friends = ({ id }: { id: string }) => {
const [users, setUsers] = useState<User[]>([]);

useEffect(() => {
const fetchFriends = async () => {
const data = await get<User[]>(`/users/${id}/friends`);
setUsers(data);
};
fetchFriends();
}, [id]);

return (
<div>
<h2>Friends</h2>
<div>
{users.map((user) => (
<Friend key={user.id} user={user} />
))}
</div>
</div>
);
};

Chúng ta có thể đơn giản sửa code để yêu cầu dữ liệu cùng một lúc trong phần Profile:

const Profile = ({ id }: { id: string }) => {
const [user, setUser] = useState<User | undefined>();
const [friends, setFriends] = useState<User[]>([]);

useEffect(() => {
const fetchUserAndFriends = async () => {
const [user, friends] = await Promise.all([
get<User>(`/users/${id}`),
get<User[]>(`/users/${id}/friends`),
]);
setUser(user);
setFriends(friends);
};
fetchUserAndFriends();
}, [id]);

return (
<>
{user && <About user={user} />}
<Friends users={friends} />
</>
);
};

Bằng cách này, chúng ta có thể loại bỏ vấn đề chuỗi yêu cầu (waterfall), và ứng dụng sẽ render nhanh hơn (vẫn phụ thuộc vào yêu cầu chậm nhất để hoàn thành, nhưng nhanh hơn trước).

Khi nào nên sử dụng
Sử dụng các yêu cầu dữ liệu song song trong các tình huống mà ứng dụng của bạn yêu cầu dữ liệu từ nhiều điểm cuối mà không phụ thuộc vào nhau. Điều này thường gặp trong các bảng điều khiển (dashboard), các biểu mẫu phức tạp, hoặc các trang hiển thị dữ liệu tổng hợp từ các nguồn khác nhau. Việc thực hiện yêu cầu song song đảm bảo rằng người dùng không phải đợi quá lâu cho các cuộc gọi mạng tuần tự hoàn thành, từ đó cải thiện độ phản hồi và hiệu suất của ứng dụng.

2. Lazy Load + Suspense (Chia mã)

Lazy loading kết hợp với Suspense của React cho phép bạn chia ứng dụng thành nhiều phần nhỏ và chỉ tải chúng khi cần thiết. Phương pháp này giảm thiểu đáng kể thời gian tải ban đầu bằng cách đảm bảo người dùng chỉ tải code cần thiết ngay từ đầu. Suspense hoạt động như một placeholder cho các component hoặc dữ liệu của bạn cho đến khi đoạn code hoặc dữ liệu cần thiết được tải về.

Chúng ta có thể sử dụng React.lazy và API Suspense để làm cho việc tải ứng dụng nhanh hơn, chỉ yêu cầu một gói JavaScript khi cần thiết.

const UserDetailCard = React.lazy(() => import("./user-detail-card.tsx"));

export const Friend = ({ user }: { user: User }) => {
return (
<Popover placement="bottom" showArrow offset={10}>
<PopoverTrigger>
<button>
<Brief user={user} />
</button>
</PopoverTrigger>
<PopoverContent>
<Suspense fallback={<div>Loading...</div>}>
<UserDetailCard id={user.id} />
</Suspense>
</PopoverContent>
</Popover>
);
};

Bạn có thể sử dụng tính năng lazy loading của React để tải động thành phần UserDetailCard trong một popover. Thành phần UserDetailCard sẽ không được tải cho đến khi thực sự cần thiết, điều này được quản lý bởi hàm React.lazy. Việc này cải thiện hiệu suất bằng cách giảm thời gian tải ban đầu của ứng dụng. Trong thành phần Friend, một popover được tạo ra với một nút kích hoạt. Khi người dùng nhấp vào nút này, popover sẽ hiển thị và thành phần UserDetailCard, được bao bọc trong một component Suspense, loads. Component Suspense cung cấp một phần tử fallback là <div>Loading...</div> cho đến khi UserDetailCard sẵn sàng hiển thị. Cấu trúc này tối ưu hóa việc tải tài nguyên và cải thiện trải nghiệm người dùng bằng cách hiển thị thông tin chi tiết người dùng trong popover khi cần.

Trình đóng gói (như Webpack hoặc Vite) sẽ đóng gói việc lazy load vào một file JavaScript riêng biệt (cùng với các phụ thuộc của nó), điều này có thể giảm lượng JavaScript phải tải khi trang được tải ban đầu.

Khi nào sử dụng

Phương pháp này lý tưởng cho các ứng dụng lớn với nhiều route và component, đặc biệt là khi một số phần của ứng dụng không cần thiết ngay lập tức cho việc tương tác ban đầu của người dùng. Hãy sử dụng lazy load và Suspense cho các component nặng, thư viện lớn hoặc các route bổ sung mà không quan trọng đối với việc render ban đầu.

3. Preload Data trước khi người dùng tương tác

Preloading dữ liệu là việc lấy dữ liệu trước khi người dùng tương tác với một component cần dữ liệu đó. Phương pháp chủ động này đảm bảo rằng dữ liệu cần thiết đã có sẵn ngay khi người dùng tương tác với component, mang lại trải nghiệm mượt mà và phản hồi nhanh chóng hơn.

import { preload } from "swr";

const UserDetailCard = React.lazy(() => import("./user-detail-card.tsx"));

const Friend = ({ user }: { user: User }) => {

const handleMouseEnter = () => {
preload(`/user/${user.id}/details`, () => getUserDetail(user.id));
};

return (
<NextUIProvider>
<Popover placement="bottom" showArrow offset={10}>
<PopoverTrigger>
<button
onMouseEnter={handleMouseEnter}
>
<Brief user={user} />
</button>
</PopoverTrigger>
<PopoverContent>
{/* UserDetailCard */}
</PopoverContent>
</Popover>
</NextUIProvider>
);
};

Các thư viện như TanStack QuerySWR đều hỗ trợ cơ chế preload này. Trong ví dụ trên, chúng ta sử dụng SWR để preload dữ liệu, vì vậy khi component thực hiện fetch dữ liệu thực tế, dữ liệu đã có sẵn — giúp cải thiện đáng kể trải nghiệm người dùng.

Khi nào sử dụng

Preload dữ liệu khi tương tác của người dùng có thể dự đoán được, và bạn có thể dự đoán dữ liệu mà họ cần tiếp theo. Phương pháp này đặc biệt hiệu quả trong các giao diện người dùng, nơi một số hành động, như di chuột hoặc nhấp vào một phần tử cụ thể, được kỳ vọng sẽ dẫn đến các tương tác phụ thuộc vào dữ liệu.

4. Static Site Generation (SSG)

Static Site Generation (SSG) là một kỹ thuật mà trong đó các trang HTML của một website được tạo ra ngay tại thời điểm build. Phương pháp này thường bao gồm dữ liệu không thay đổi thường xuyên, cho phép các trang được phục vụ nhanh chóng mà không cần tính toán từ phía server hay truy vấn cơ sở dữ liệu trong thời gian chạy.

Ví dụ, giả sử tôi có một ứng dụng "Quote of the Day" (Câu nói trong ngày), tại thời điểm build, chúng ta có thể pre-render một vài câu nói cho người dùng để họ có thể thấy ngay khi lần đầu tiên truy cập ứng dụng.

Ví dụ dưới đây, ta đang sử dụng Next.js để điền dữ liệu vào trang chủ:

async function getQuotes(): Promise<QuoteType[]> {
const res = await fetch(
"https://api.quotable.io/quotes/random?tags=happiness,famous-quotes&limit=3"
);

return res.json();
}

export default async function Home() {
const quotes = await getQuotes();
return (
<Quote initQuotes={quotes} />
);
}

Trong Next.js, yêu cầu trên sẽ được gửi trong quá trình build, và khi người dùng truy cập ứng dụng trên trình duyệt, các câu trích dẫn sẽ đã được nhúng sẵn trong HTML.

Khi nào nên sử dụng

Static Site Generation (SSG) phù hợp nhất với các nội dung ít thay đổi, chẳng hạn như bài blog, tài liệu hướng dẫn, danh sách sản phẩm thương mại điện tử hoặc trang landing page. Phương pháp này giúp cải thiện hiệu suất bằng cách pre-render nội dung và phân phối trực tiếp từ CDN, giảm tải cho máy chủ và tăng tốc độ hiển thị nội dung.

5. React Server Component (Streaming)

React Server Components cho phép render các component ở phía server và stream chúng tới client. Phương pháp này giúp chỉ gửi dữ liệu cần thiết và mã JavaScript tối thiểu tới client, giảm thiểu lượng mã cần tải về, phân tích cú pháp và thực thi, từ đó rút ngắn thời gian phản hồi khi người dùng tương tác.

Cải tiến ví dụ Profile sử dụng React Server Components

Giả sử chúng ta có một ví dụ với component Profile, thay vì tải dữ liệu trên client, chúng ta có thể render dữ liệu đó ngay trên server và chỉ gửi dữ liệu đã render tới client, từ đó giảm bớt thời gian tải ban đầu.

Ví dụ về cách sử dụng React Server Components:

import { Suspense } from "react";

async function UserInfo({ id }: { id: string }) {
const user = await getUser(id);

return (
<>
<About user={user} />
<Suspense fallback={<FeedsSkeleton />}>
<Feeds category={user.interests[0]} />
</Suspense>
</>
);
}

async function Friends({ id }: { id: string }) {
const friends = await getFriends(id);

return (
<div>
<h2>Friends</h2>
<div>
{friends.map((user) => (
<Friend user={user} key={user.id} />
))}
</div>
</div>
);
}

Đoạn mã này trình bày hai component React bất đồng bộ, UserInfoFriends, thực hiện việc fetch và hiển thị thông tin liên quan đến người dùng.

UserInfo Component

Trong UserInfo, một hàm bất đồng bộ gọi getUser(id) để lấy thông tin người dùng. Component này sẽ render một component <About> với dữ liệu người dùng đã fetch được và một component <Feeds> bên trong một block <Suspense>. Component <Suspense> sử dụng <FeedsSkeleton> như một fallback, hiển thị khi <Feeds> vẫn đang tải thông tin về sở thích của người dùng.

Friends Component

Component Friends, cũng là một component bất đồng bộ, sẽ lấy danh sách bạn bè cho một ID người dùng nhất định thông qua hàm getFriends(id). Sau đó, nó render một danh sách bạn bè, mỗi người bạn sẽ được hiển thị bằng cách sử dụng component <Friend>, với dữ liệu người dùng riêng biệt được truyền vào. Hàm map sẽ lặp qua mảng bạn bè, render mỗi item dưới dạng một <Friend> component với thuộc tính key duy nhất.

export async function Profile({ id }: { id: string }) {
return (
<div>
<h1>Profile</h1>
<div>
<div>
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo id={id} />
</Suspense>
</div>
<div>
<Suspense fallback={<FriendsSkeleton />}>
<Friends id={id} />
</Suspense>
</div>
</div>
</div>
);
}

Trong thiết lập này, Profile là một component bất đồng bộ dùng để hiển thị thông tin hồ sơ người dùng. Nó chứa hai component con, là UserInfo Friends, mỗi component đều được bọc trong Suspense. Các Suspense components này cung cấp giao diện dự phòng tương ứng (UserInfoSkeleton FriendsSkeleton) trong khi dữ liệu cho UserInfo Friends đang được tải.

Khi Profile, UserInfo Friends React Server Components

  • Thực thi trên Server: Các component này sẽ chạy trên server, có nghĩa là việc fetch dữ liệu người dùng và danh sách bạn bè sẽ do server xử lý, thay vì trình duyệt của người dùng.

  • Streaming đến Client: Sau khi thực thi trên server, chỉ HTML cần thiết và JavaScript tối thiểu được gửi đến client. React Server Components có thể stream nội dung đã render trực tiếp, giúp người dùng thấy dữ liệu ngay mà không cần tải thêm logic nặng ở phía client.

  • Cải thiện hiệu suất: Cách tiếp cận này giúp giảm tải JavaScript cần tải xuống, tăng tốc độ tải trang và cải thiện trải nghiệm người dùng do trình duyệt không cần tự fetch dữ liệu hoặc render các component phức tạp.

Hiện trạng và mức độ hỗ trợ

Hiện tại, React Server Components vẫn chưa được áp dụng rộng rãi trong tất cả các framework. Việc sử dụng còn hạn chế và chủ yếu được hỗ trợ trong một số framework như Next.js.

Lý do hạn chế:

a, Đang ở giai đoạn thử nghiệm: Đây là một hướng đi mới trong phát triển React, cần có thời gian kiểm chứng trước khi được chấp nhận rộng rãi.

b, Hỗ trợ framework còn hạn chế: Chỉ một số framework như Next.js hỗ trợ sẵn do yêu cầu về server-side rendering và hạ tầng chuyên biệt.

c, Hệ sinh thái cần điều chỉnh: Các công cụ, thư viện và quy trình phát triển cần thích ứng với mô hình mới này.

d, Độ phức tạp cao: React Server Components giới thiệu nhiều khái niệm mới, yêu cầu lập trình viên nắm vững mô hình SSR và streaming rendering.

Khi nào nên sử dụng?

Mặc dù còn mới mẻ, React Server Components rất hữu ích trong các trường hợp sau:

  • Ứng dụng có khối lượng dữ liệu lớn hoặc cần xử lý phức tạp.

  • Muốn giảm tải tác vụ xử lý trên client để cải thiện hiệu suất.

  • Dự án sử dụng Next.js, nơi React Server Components được hỗ trợ tốt.

Khi công nghệ này phát triển hơn và được tích hợp vào hệ sinh thái React, chúng ta có thể mong đợi sự hỗ trợ rộng rãi hơn trong nhiều framework và công cụ khác.

Lời kết

VietnamWorks inTECH hy vọng rằng, thông qua bài viết này bạn đã bỏ túi được một vài kỹ thuật sẽ giúp bạn xây dựng các ứng dụng React nhanh hơn, mượt hơn và tối ưu hơn cho người dùng. 

Nguồn: Juntao Qiu

VietnamWorks inTECH