Tìm hiểu useCallback, khi nào thì dùng useCallback?
Cách đây ít lâu, có bạn nói tôi vầy “nếu một list bị re-render sau khi update state thì nên sử dụng useCallback, vì useCallback là best practice của react nhằm tránh re-render lại component”, việc này nghe ra thì cũng có vẻ hợp lý, vì mục đích của useCallback là giúp chúng ta tối ưu hiệu suất. Nhưng sử dụng useCallback một cách hiệu quả là câu chuyện khác.
Trong React, khi một react function component, nếu nó có chứa một hàm bên trong nó, thì hàm đó sẽ render mỗi lần component đó render. Đây là một hành vi bình thường của React, phần lớn các trường hợp, thì việc tái tạo lại dăm ba cái inline function lẻ tẻ không ảnh hưởng gì mấy đến hiệu suất khi render, vì vậy việc re-render đó hoàn toàn chấp nhận được. Tài liệu của react có đề cập như sau
Hooks có chậm vì nó tạo ra hàm trong quá trình render không?
Không. Với các trình duyệt hiện đại, hiệu suất căn bản của closure so với class không có sự khác biệt trừ hoàn cảnh khắc nghiệt.
Vậy, chúng ta sẽ cùng tìm hiểu sâu hơn mục đích của useCallback và cách sử dụng nó sao cho hợp lý.
Theo tài liệu chính thức, cách thức hoạt động của useCallback được mô tả tương đối ngắn gọn như sau:
“Trả về một hàm được ghi nhớ. Khi truyền một hàm inline callback và dependencies, useCallback sẽ trả lại một phiên bản của hàm callback đã được ghi nhớ và hàm đó chỉ thay đổi nếu một trong những dependencies đã bị thay đổi. Việc này hữu dụng khi truyền các hàm callback vào các component con đã tối ưu dựa trên các bình đẳng tham chiếu (reference equality) để ngăn các render không cần thiết”.
Bình đẳng tham chiếu trong JavaScript được biết như sau (react dùng Object.is nhưng dùng === cũng kết quả tương tự)
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
function foo() {return 0 }
function bar() { return 0 }
foo === bar; // false
foo === foo; // true
Với useCallback thì nó sẽ ghi nhớ hàm đã truyền vào và trả hàm khi nhớ trong trường hợp dependencies không thay đổi, chúng ta sẽ thấy hành vi hàm useCallback thực thi như sau:
- Trong lần đầu component được render, useCallback sẽ ghi nhớ hàm được truyền vào nó.
- Với mỗi lần render tiếp theo, useCallback sẽ thực hiện phép so sánh tham chiếu bằng hàm Object.is để đối chiếu sự thay đổi của các dependencies truyền vào nó, nếu không có thay đổi, hàm truyền vào sẽ hoàn toàn bị bỏ qua.
- Khi có sự thay đổi dependencies, useCallback sẽ thay thế hàm bạn đã truyền vào nó thay cho hàm đã lưu trước đó.
Dựa trên hoạt động của useCallback , chúng ta có thể thấy là trên thực tế nó phải làm một số tác vụ để hoàn thành nhiệm vụ của nó là ghi nhớ, so sánh và trả về hàm callback.
Demo
Mời các bạn xem demo sau
Để minh họa sự khác nhau giữa không dùng useCallback và có useCallback, chúng ta thực hiện một demo todo list đơn giản, nó bao gồm một trường input để nhập task todo: sau mỗi lần submit, bên dưới danh sách sẽ hiển thị các task đã được tạo.
Để thực hiện việc này thì ta có các component sau:
- TaskInput là trường nhập task và submit task
- TaskListItem là component của mỗi task
- ActionGroup là component con của TaskListItem, mỗi khi thay đổi trạng thái thì ActionGroup cũng thay đổi cách hiển thị (cái này thì cũng không cần quan tâm lắm).
Không có useCallback
Toàn bộ list task re-render sau mỗi lần submit 1 task.
Profiler của react dev tool cho ta thấy thời gian render của toàn bộ component tính từ lúc state update là 11.8ms
Có useCallback
Chúng ta sẽ thay đổi phương thức submit bằng cách sử dụng hàm useCallback, và trường hợp này chúng ta cần truyền giá trị tasks như là dependency của useCallback gì nó là giá trị thay đổi.
Profiler cho ta thấy mức sai số thì nó chưa có gì đáng kể, khoan đã, tuy nhiên chúng ta vẫn thấy là danh sách của todo task vẫn bị re-render, lý do là vì danh sách task thay đổi thì props của TaskListItem cập nhật khi thêm 1 task vào danh sách, vì vậy ta cần phải bổ sung thêm memo cho TaskListItem để ghi nhớ giá trị cho các task đã có trước đó, react sẽ không render nữa (memo cũng sử dụng phép so sánh tham chiếu)
Ta có thể thấy thời gian render cải thiện cùng với việc hạn chế render các task đã tạo ra.
Ảnh hưởng đến trải nghiệm người dùng?
Theo như mô hình RAIL (https://web.dev/rail/) nghiên cứu của nhóm phát triển cải thiện hiệu suất của google, thời gian tương tác trên trình duyệt nên giới hạn trong khoảng 0-100ms, trong đó thời gian phản hồi tối ưu trong khoảng 0-50ms .
Vì vậy, việc chênh nhau vài ms cũng chẳng ảnh hưởng đến trải nghiệm người dùng lắm, các lập trình viên React thường hay bị ám ảnh về chuyện re-render của component chứ bản chất việc re-render nó bình thường.
Vậy thì khi nào ta sử dụng useCallback, demo có phải tối ưu không?
Như tôi đã đề cập ở trên, trong nhiều trường hợp thì bạn cũng tìm cách tránh dùng useCallback thường xuyên, bởi vì ghi nhớ mọi thứ không cần thiết sẽ, tốt nhất là tìm giải pháp khác hợp lý hơn. Trong trường hợp cần thiết và đôi khi là bắt buộc, thì bạn nên sử dụng công cụ profiler để kiểm tra trong quá trình code để tối ưu những cái quan trọng. Xét về thực tế tổng quan, để tối ưu một ứng dụng bạn nên sử dụng công cụ Chrome devtools hay tương tự để có thể phân tích sâu hơn bản build production để tránh lan man không cần thiết.
useCallback được sử dụng trong các trường hợp sau.
- Render một danh sách dữ liệu lớn, lên đến hàng trăm hạng mục.
Trên thực tế, cũng có giải pháp khác nhau để giải quyết vấn đề dính đến dữ liệu lớn làm cho ảnh hưởng đến hiệu suất ở cả phía back-end lẫn font-end, ví dụ như phân trang infinity load... Riêng với frontend, render một danh sách dữ liệu lớn thì bạn nên dùng phương pháp windowing chỉ render vùng “thấy được” trên màn hình, có một số thư viện phổ biến như reac-virtualized hay react-window.
- Những tính toán tiêu tốn chi phí, ví dụ như là các tương tác liên quan đến tương tác biểu đồ, đồ thị, hay là animation.
Với cá nhân tôi, trong một dự án gần nhất, có một thao tác liên quan đến việc tính toán thông tin của toàn bộ list khi click vào xem một danh sách, lúc đó bắt buộc tôi phải dùng useCallback và memo để tối ưu hiệu suất.
Và như ví dụ trên, ta cũng thấy useCallback hay có sự phối hợp với memo, useMemo để mang lại kết quả như mong muốn.
Với demo đơn giản như chúng ta vừa làm, ta có thể kết luận tiết kiệm được vài ms mà không có sự khác biệt thì không cần sự có mặt useCallback để làm cái việc so sánh tham chiếu không cần thiết.
Kết luận
Suy cho cùng, việc tối ưu hiệu suất của một ứng dụng nó luôn là xu hướng mà bất kỳ người lập trình nào cũng muốn thực hiện cho ứng dụng của mình, và có nhiều thứ để chúng ta phải tối ưu chứ không phải chuyện re-render, quan điểm của tôi là một khi bạn muốn tối ưu thì chỉ cần trả lời câu hỏi khi nào, tại sao, việc “tối ưu” nhiều lúc cũng ảnh hưởng đến tiến độ, tôi từng thấy nhiều người tốn thời gian chỉ để tiết kiệm vài ms không đáng kể trong khi có những việc quan trọng hơn cần giải quyết trước. “Best practice” nghĩa là dùng đúng chỗ, đúng lúc. Một điều tôi học hỏi được từ công việc và từ kinh nghiệm thực tế là khi thực hiện triển khai một ứng dụng, bạn có thể dùng kinh nghiệm bản thân để lựa chọn giải pháp tối ưu phù hợp, nhưng cũng đừng vội tối ưu nếu ứng dụng chưa đủ ổn định và bạn chưa đo lường về cái mình cần tối ưu là gì.
Source demo tham khảo
https://github.com/viiiprock/blog-examples/tree/main/when-to-use-useCallback