Bạn đã bao giờ trải nghiệm một ứng dụng web mà khi bạn gõ vào ô tìm kiếm, các ký tự lại hiển thị một cách chậm chạp, giật lag? Hoặc khi một danh sách dài đang được tải, toàn bộ trang web dường như "đóng băng", không thể tương tác? Đây là những vấn đề kinh điển về hiệu năng mà mọi nhà phát triển đều trăn trở.
React, với sự phát triển vượt bậc của mình, đã giới thiệu hai khái niệm mang tính cách mạng để giải quyết triệt để vấn đề này: Time Slicing và Scheduling. Đây không chỉ là những thuật ngữ kỹ thuật khô khan, mà là "siêu năng lực" giúp React tạo ra những trải nghiệm người dùng mượt mà đến không ngờ.
Trong bài viết này, chúng ta sẽ cùng nhau vén bức màn bí mật, khám phá từ "tại sao" chúng ra đời cho đến "làm thế nào" để tận dụng sức mạnh của chúng trong các dự án của bạn.
1. Vấn đề của quá khứ: Tại sao React cần thay đổi?
Để hiểu được sự vi diệu của Time Slicing, chúng ta cần quay lại với phiên bản cũ của React. Trước đây, quá trình render của React hoạt động theo cơ chế đồng bộ (synchronous).
Hãy tưởng tượng React là một người đầu bếp đang thực hiện một công thức rất dài. Với cơ chế cũ (được gọi là Stack Reconciler), người đầu bếp này sẽ bắt đầu nấu và không dừng lại cho đến khi hoàn thành toàn bộ món ăn. Nếu có khách hàng (người dùng) đến và gọi món khác (ví dụ: click vào một nút), họ sẽ phải chờ cho đến khi người đầu bếp nấu xong món hiện tại.
Trong thế giới trình duyệt, "người đầu bếp" này chính là main thread (luồng chính). Khi React thực hiện một tác vụ render lớn (ví dụ: cập nhật một danh sách 10,000 mục), nó sẽ chiếm dụng hoàn toàn luồng chính. Vì luồng chính cũng chịu trách nhiệm xử lý các tương tác của người dùng (như click, cuộn trang, gõ phím), nên khi nó bị chặn, toàn bộ trang web sẽ trở nên "đơ" và không phản hồi.
Đây chính là lúc cuộc cách mạng React Fiber ra đời. Fiber là một sự viết lại hoàn toàn thuật toán cốt lõi của React, đặt nền móng cho khả năng render bất đồng bộ (asynchronous) và chính là tiền đề cho Time Slicing và Scheduling.
2. Time Slicing - "Thái lát" công việc 🎬
Time Slicing là cơ chế cho phép React chia nhỏ một tác vụ render lớn thành nhiều "mẩu" công việc nhỏ hơn. Thay vì thực hiện một lèo không nghỉ, React giờ đây sẽ thực hiện từng mẩu công việc trong một khoảng thời gian rất ngắn (vài mili giây).
Sau khi hoàn thành một mẩu, React sẽ "nhường" lại quyền kiểm soát cho trình duyệt. Nó sẽ hỏi: "Này trình duyệt, có việc gì quan trọng hơn cần làm ngay bây. giờ không? Ví dụ như xử lý một cú click chuột hay một phím bấm chẳng hạn?"
- Nếu có việc quan trọng hơn, React sẽ tạm dừng công việc render hiện tại để ưu tiên xử lý tương tác của người dùng.
- Nếu không, React sẽ tiếp tục thực hiện mẩu công việc tiếp theo.
Quá trình này cứ lặp đi lặp lại cho đến khi toàn bộ tác vụ render được hoàn thành.
Nói một cách dễ hiểu: React không còn là người đầu bếp "làm một lèo" nữa, mà đã trở thành một đạo diễn phim tài ba. Vị đạo diễn này có thể quay một cảnh ngắn, sau đó dừng lại để xem có diễn viên nào cần nghỉ ngơi không, rồi mới tiếp tục quay cảnh tiếp theo. Kết quả là cả đoàn phim (ứng dụng) hoạt động một cách hài hòa và không ai bị "quá tải".
Lợi ích cốt lõi của Time Slicing: Nó đảm bảo rằng luồng chính không bao giờ bị chặn quá lâu, giúp ứng dụng luôn phản hồi nhanh chóng với các tương tác của người dùng, ngay cả khi đang thực hiện các tác vụ render nặng nề.
3. Scheduling - "Xếp lịch" công việc theo độ ưu tiên 👨⚕️
Nếu Time Slicing là cơ chế "thái lát" công việc, thì Scheduling chính là bộ não quyết định mẩu công việc nào nên được thực hiện trước.
Không phải tất cả các cập nhật trong một ứng dụng đều có tầm quan trọng như nhau. Hãy tưởng tượng bạn đang ở trong một phòng cấp cứu:
- Độ ưu tiên cao (khẩn cấp): Bệnh nhân bị ngừng tim. Cần được xử lý ngay lập tức!
- Độ ưu tiên thấp (có thể chờ): Bệnh nhân bị trầy xước nhẹ. Có thể chờ một chút cũng không sao.
Trong React cũng vậy:
- Cập nhật ưu tiên cao: Phản hồi từ việc gõ phím của người dùng. Người dùng mong đợi thấy ký tự xuất hiện ngay lập tức khi họ gõ.
- Cập nhật ưu tiên thấp (Transition): Hiển thị kết quả tìm kiếm sau khi người dùng gõ xong. Việc này có thể trễ một vài trăm mili giây mà không làm ảnh hưởng xấu đến trải nghiệm.
Scheduler của React sẽ tự động phân loại các cập nhật này và ưu tiên thực hiện những tác vụ quan trọng trước. Nhờ vậy, ứng dụng của bạn sẽ luôn cho cảm giác "nhanh" và "tức thì" ở những nơi cần thiết nhất.
4. Biến lý thuyết thành thực tế: Các API mạnh mẽ
React cung cấp cho chúng ta những công cụ mạnh mẽ để có thể "chỉ đạo" cho Scheduler biết đâu là cập nhật ưu tiên thấp. Hai API phổ biến nhất là startTransition
và useDeferredValue
.
a. startTransition
API này cho phép bạn đánh dấu một hoặc nhiều cập nhật state là "không khẩn cấp".
Hãy xem ví dụ về ô tìm kiếm kinh điển:
import { useState, useTransition } from 'react'
function App() {
const [isPending, startTransition] = useTransition()
const [inputValue, setInputValue] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const handleChange = (e) => {
// 1. Cập nhật giá trị ô input (ƯU TIÊN CAO)
setInputValue(e.target.value)
// 2. Bọc cập nhật state cho danh sách kết quả trong startTransition
// React sẽ hiểu đây là một "Transition" (ƯU TIÊN THẤP)
startTransition(() => {
setSearchQuery(e.target.value)
})
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
{/* isPending cho biết transition có đang diễn ra hay không */}
{isPending ? <div>Đang tải danh sách...</div> : null}
{/* Một component danh sách rất lớn, render chậm */}
<MySlowListComponent query={searchQuery} />
</div>
)
}
Trong ví dụ trên:
- Việc cập nhật
inputValue
diễn ra ngay lập tức, giúp người dùng cảm thấy ô input luôn phản hồi. - Việc cập nhật
searchQuery
(thứ gây ra việc render lại danh sách lớn) được bọc trongstartTransition
. React sẽ thực hiện việc này ở chế độ ưu tiên thấp, không làm "đơ" ô input. - Biến
isPending
là một công cụ tuyệt vời để hiển thị một thông báo loading trong khi quá trình render ưu tiên thấp đang diễn ra.
b. useDeferredValue
useDeferredValue
là một hook tiện lợi khác để đạt được hiệu quả tương tự, nhưng theo một cách tiếp cận hơi khác. Nó cho phép bạn "trì hoãn" một giá trị. React sẽ render với giá trị cũ trước, sau đó thử render lại với giá trị mới ở chế độ nền.
import { useState, useDeferredValue } from 'react'
function App() {
const [query, setQuery] = useState('')
// 1. deferredQuery sẽ "trì hoãn" việc cập nhật theo query
const deferredQuery = useDeferredValue(query)
const handleChange = (e) => {
setQuery(e.target.value)
}
// 2. So sánh query và deferredQuery để biết có đang chờ cập nhật không
const isStale = query !== deferredQuery
return (
<div>
<input type="text" value={query} onChange={handleChange} />
{isStale ? <div>Đang tải danh sách...</div> : null}
{/* Component danh sách sẽ nhận giá trị đã được trì hoãn */}
<MySlowListComponent query={deferredQuery} />
</div>
)
}
useDeferredValue
đặc biệt hữu ích khi bạn không có quyền kiểm soát mã nguồn nơi state được cập nhật (ví dụ: giá trị đến từ một thư viện bên ngoài).
Kết luận: Tương lai của hiệu năng React
Time Slicing và Scheduling không phải là những tính năng bạn phải cấu hình phức tạp. Chúng là nền tảng của chế độ Concurrent Rendering (Render đồng thời) trong React, hoạt động một cách âm thầm để mang lại hiệu quả tốt nhất.
Bằng cách hiểu và sử dụng các API như startTransition
và useDeferredValue
, bạn có thể:
- ⚡️ Cải thiện trải nghiệm người dùng: Loại bỏ hoàn toàn tình trạng UI bị "đóng băng", giúp ứng dụng luôn mượt mà và phản hồi nhanh nhạy.
- ⚡️ Tối ưu hóa hiệu năng: Cho phép trình duyệt xử lý các tác vụ quan trọng ngay lập tức, trong khi các tác vụ nặng nề hơn được thực hiện ở chế độ nền.
- ⚡️ Viết code hiệu năng một cách tự nhiên: Bạn không cần phải đau đầu với các kỹ thuật tối ưu hóa phức tạp như
debounce
haythrottle
trong nhiều trường hợp nữa.
Nắm vững hai khái niệm này chính là chìa khóa để bạn mở ra một đẳng cấp mới trong việc xây dựng các ứng dụng React hiện đại, hiệu năng cao và mang lại trải nghiệm tuyệt vời cho người dùng. Hãy bắt đầu áp dụng chúng ngay hôm nay nhé!