Giới thiệu: Hiệu năng React và câu chuyện Memoization
React, với cơ chế Virtual DOM thông minh, thường giúp chúng ta xây dựng giao diện người dùng nhanh chóng. Tuy nhiên, không phải lúc nào mọi thứ cũng "mượt mà" như mong đợi. Đôi khi, các component của bạn có thể render lại quá nhiều lần, gây lãng phí tài nguyên và ảnh hưởng đến trải nghiệm người dùng. Đây là lúc chúng ta cần đến các "công cụ" tối ưu hiệu năng như useMemo và useCallback.
Nhưng liệu chúng có phải là "viên đạn bạc" cho mọi vấn đề hiệu năng? Hay đôi khi chúng lại trở thành gánh nặng? Hãy cùng tìm hiểu!
useMemo và useCallback là gì? Hiểu đúng bản chất Memoization
Cả useMemo và useCallback đều dựa trên một khái niệm cốt lõi: memoization (ghi nhớ). Hiểu đơn giản, memoization là kỹ thuật lưu trữ kết quả của một hàm tốn kém để, khi hàm đó được gọi lại với cùng các đối số, thay vì thực hiện lại phép tính, nó sẽ trả về kết quả đã lưu.
useMemo(callback, dependencies): Dùng để ghi nhớ một giá trị. Nó sẽ chỉ tính toán lại giá trị mới khi một trong cácdependencies(phụ thuộc) thay đổi.useCallback(callback, dependencies): Dùng để ghi nhớ một hàm (callback). Nó sẽ chỉ trả về một phiên bản hàm mới khi một trong cácdependenciesthay đổi. Mục đích chính là để đảm bảo tham chiếu của hàm không đổi giữa các lần render.
Khi nào NÊN sử dụng useMemo?
useMemo phát huy tác dụng tốt nhất trong các trường hợp sau:
- Tính toán tốn kém tài nguyên: Nếu bạn có một phép tính phức tạp, mất nhiều thời gian hoặc tài nguyên để thực hiện (ví dụ: lọc, sắp xếp một mảng lớn, tính toán đồ họa),
useMemocó thể giúp bạn tránh lặp lại phép tính đó trong mỗi lần render.function BigCalculationComponent({ data }) {{ const expensiveResult = useMemo(() => {{ console.log("Thực hiện phép tính tốn kém..."); // Giả lập một phép tính tốn kém return data.filter(item => item.value > 10).map(item => item.value * 2); }}, [data]); // Chỉ tính toán lại khi 'data' thay đổi return ( <div> <p>Kết quả phép tính: {expensiveResult.join(', ')}</p> </div> );}} - Truyền Object/Array làm props cho component con được memoized: Khi bạn truyền một object hoặc array mới được tạo ra trong mỗi lần render xuống một component con được bọc bởi
React.memo, component con đó sẽ render lại dù giá trị bên trong object/array không đổi.useMemogiúp duy trì tham chiếu của object/array đó.const ChildComponent = React.memo(({ config }) => {{ console.log("ChildComponent render"); return <div>{config.theme}</div>;}});function ParentComponent() {{ const [count, setCount] = useState(0); // Mỗi lần ParentComponent render, object này sẽ được tạo mới // const config = {{ theme: 'dark', size: 'medium' }}; // Dùng useMemo để giữ nguyên tham chiếu của 'config' const config = useMemo(() => ({{ theme: 'dark', size: 'medium' }}), []); return ( <div> <button onClick={() => setCount(count + 1)}>Tăng Count: {count}</button> <ChildComponent config={config} /> </div> );}}
Khi nào KHÔNG NÊN sử dụng useMemo?
Đừng lạm dụng useMemo, vì nó cũng có chi phí:
- Phép tính đơn giản: Đối với các phép tính nhỏ, chi phí để
useMemolưu trữ giá trị và kiểm tra dependency có thể lớn hơn hoặc bằng chi phí thực hiện lại phép tính. - Overhead không cần thiết: Mỗi lần sử dụng
useMemo, React cần cấp phát bộ nhớ để lưu giá trị và so sánh các dependency. Nếu không thực sự cần, bạn đang thêm gánh nặng không đáng có. - Khi
React.memođã đủ: Nếu vấn đề là component con render lại, hãy xem xétReact.memotrước.useMemochỉ hữu ích khi bạn cần memoize một giá trị cụ thể được truyền xuống.
Khi nào NÊN sử dụng useCallback?
useCallback đặc biệt hữu ích khi bạn cần đảm bảo tham chiếu của một hàm không thay đổi:
- Truyền hàm làm props cho component con được memoized: Đây là trường hợp phổ biến nhất. Tương tự như object/array, nếu bạn truyền một hàm mới được tạo ra trong mỗi lần render xuống một component con được bọc bởi
React.memo, component con đó sẽ render lại.useCallbackgiải quyết vấn đề này.const ButtonComponent = React.memo(({ onClick }) => {{ console.log("ButtonComponent render"); return <button onClick={onClick}>Nhấn tôi</button>;}});function ParentComponent() {{ const [count, setCount] = useState(0); // Mỗi lần ParentComponent render, hàm này sẽ được tạo mới // const handleClick = () => setCount(count + 1); // Dùng useCallback để giữ nguyên tham chiếu của 'handleClick' const handleClick = useCallback(() => {{ setCount(prevCount => prevCount + 1); }}, []); // Hàm này sẽ không thay đổi trừ khi dependencies thay đổi return ( <div> <p>Count: {count}</p> <ButtonComponent onClick={handleClick} /> </div> );}} - Dependencies trong các Hooks khác: Nếu bạn có một hàm được sử dụng làm dependency trong
useEffect,useLayoutEffect, hoặc các hook khác, và bạn muốn ngăn chặn việc hook đó chạy lại không cần thiết,useCallbacksẽ rất hữu ích.function DataFetcher() {{ const [data, setData] = useState(null); const fetchData = useCallback(async () => {{ // Giả lập gọi API const response = await fetch('/api/data'); const result = await response.json(); setData(result); }}, []); // Chỉ tạo lại hàm khi các dependencies thay đổi useEffect(() => {{ fetchData(); }}, [fetchData]); // `fetchData` là dependency, cần được memoize return <div>{data ? JSON.stringify(data) : 'Đang tải...'}</div>;}}
Khi nào KHÔNG NÊN sử dụng useCallback?
Tương tự useMemo, useCallback cũng có chi phí và không phải lúc nào cũng cần thiết:
- Component con không được memoized: Nếu component con không được bọc bởi
React.memo, việc truyền một hàm memoized sẽ không mang lại lợi ích gì vì component con vẫn sẽ render lại. - Hàm đơn giản, không tốn kém: Đối với các hàm nhỏ, không gây ra vấn đề hiệu năng, chi phí của
useCallback(lưu trữ và so sánh dependency) có thể lớn hơn lợi ích mang lại. - Overhead không cần thiết: Tương tự
useMemo, việc sử dụnguseCallbackmột cách không suy nghĩ sẽ dẫn đến việc tiêu tốn bộ nhớ và thời gian xử lý không cần thiết.
Lời khuyên quan trọng: Đừng tối ưu hóa quá sớm!
Một nguyên tắc vàng trong lập trình là "Đừng tối ưu hóa quá sớm". Hãy viết code của bạn một cách rõ ràng và dễ đọc trước. Chỉ khi bạn thực sự thấy có vấn đề về hiệu năng (thường thông qua công cụ như React DevTools Profiler), hãy nghĩ đến việc sử dụng useMemo và useCallback.
Hãy nhớ rằng, chúng là các công cụ mạnh mẽ, nhưng cần được sử dụng một cách có ý thức và đúng chỗ để thực sự cải thiện hiệu năng, thay vì tạo ra gánh nặng không đáng có.
Kết luận
useMemo và useCallback là những hook hữu ích trong React giúp chúng ta tối ưu hiệu năng bằng cách tránh các phép tính tốn kém hoặc ngăn chặn các component con render lại không cần thiết. Tuy nhiên, việc sử dụng chúng một cách thiếu cân nhắc có thể gây ra nhiều vấn đề hơn là giải quyết. Hãy hiểu rõ bản chất của memoization, và áp dụng chúng một cách thông minh, có chọn lọc, luôn dựa trên dữ liệu profiling thực tế. Chúc bạn code React "mượt mà"!