[React Basics] useCallback và useMemo: Hiểu rõ cách tối ưu hiệu năng React App

Nếu bạn đã từng làm việc với React, chắc hẳn bạn đã nghe đến cuộc chiến không hồi kết chống lại "kẻ thù" mang tên re-render không cần thiết. Đây chính là lúc useCallbackuseMemo xuất hiện như những vị cứu tinh, giúp ứng dụng của bạn chạy mượt mà, nhanh chóng và hiệu quả hơn.

useCallback và useMemo: Hiểu rõ cách tối ưu hiệu năng React App

Nhưng chính xác thì chúng là gì? Chúng hoạt động ra sao? Và quan trọng nhất, khi nào chúng ta nên và không nên sử dụng chúng? Hãy cùng nhau vén bức màn bí ẩn về "bộ đôi quyền lực" này nhé!

Nguồn gốc của vấn đề: Tại sao re-render lại là "kẻ xấu"?

Để hiểu được sức mạnh của useCallbackuseMemo, trước tiên chúng ta cần hiểu rõ "kẻ thù" mà chúng ta đang đối mặt. Trong React, một component sẽ re-render (vẽ lại giao diện) khi state hoặc props của nó thay đổi.

Vấn đề nằm ở cách JavaScript xử lý sự bằng nhau (equality) của các đối tượng và hàm.

// Mỗi lần so sánh, dù có vẻ giống hệt nhau, chúng lại là 2 đối tượng khác nhau trong bộ nhớ!
console.log({} === {}) // false
console.log([] === []) // false
console.log((() => {}) === (() => {})) // false

Điều này có nghĩa là gì trong React? Mỗi khi một component cha re-render, tất cả các hàm và đối tượng được định nghĩa bên trong nó sẽ được tạo mới. Mặc dù mã nguồn của hàm đó không thay đổi, nhưng tham chiếu (reference) của nó trong bộ nhớ đã khác đi.

Khi bạn truyền các hàm hoặc đối tượng mới này xuống component con dưới dạng props, React sẽ nghĩ rằng "Ồ, props đã thay đổi!" và khiến component con đó re-render một cách không cần thiết. Đây chính là lúc hiệu năng ứng dụng của bạn bắt đầu bị ảnh hưởng.

useMemo: Khi bạn muốn "ghi nhớ" một giá trị đắt giá 🧠

useMemo là một hook cho phép bạn ghi nhớ (memoize) kết quả của một phép tính tốn kém. Nó sẽ chỉ tính toán lại giá trị đó khi một trong các phụ thuộc (dependencies) của nó thay đổi.

Cú pháp:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • Hàm tính toán: () => computeExpensiveValue(a, b) là nơi bạn thực hiện các phép tính phức tạp (lọc mảng lớn, tính toán toán học phức tạp...).
  • Mảng phụ thuộc: [a, b] là danh sách các giá trị mà useMemo sẽ theo dõi. Nếu bất kỳ giá trị nào trong mảng này thay đổi, hàm tính toán sẽ được chạy lại. Nếu không, useMemo sẽ trả về giá trị đã được ghi nhớ từ lần render trước.

Khi nào nên dùng useMemo?

  1. Tối ưu hóa các tính toán nặng: Khi bạn có một hàm xử lý dữ liệu lớn và tốn nhiều thời gian, bọc nó trong useMemo để tránh phải chạy lại trên mỗi lần re-render.
  2. Duy trì tham chiếu cho đối tượng/mảng: Khi bạn cần truyền một đối tượng hoặc mảng xuống component con, hãy dùng useMemo để đảm bảo rằng tham chiếu của đối tượng/mảng đó không thay đổi nếu dữ liệu bên trong nó không đổi.

Ví dụ thực tế:

Hãy tưởng tượng bạn có một danh sách sản phẩm và một chức năng lọc các sản phẩm đắt tiền.

// Trước khi dùng useMemo: Hàm filter chạy mỗi khi component re-render
const expensiveProducts = products.filter((product) => product.price > 1000)

// Sau khi dùng useMemo: Hàm filter chỉ chạy lại khi danh sách 'products' thay đổi
const expensiveProducts = useMemo(() => {
  console.log('Đang lọc sản phẩm đắt tiền...') // Bạn sẽ thấy log này ít xuất hiện hơn
  return products.filter((product) => product.price > 1000)
}, [products])

useCallback: Khi bạn muốn "ghi nhớ" một hàm 📜

Rất giống với useMemo, nhưng thay vì ghi nhớ kết quả trả về của một hàm, useCallback ghi nhớ chính bản thân hàm đó.

Cú pháp:

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])

Nói một cách đơn giản, useCallback trả về một phiên bản "bất biến" của hàm. Phiên bản này chỉ thay đổi khi một trong các phụ thuộc trong mảng [a, b] thay đổi.

Một sự thật thú vị: useCallback(fn, deps) tương đương với useMemo(() => fn, deps). useCallback thực chất chỉ là một lối viết tắt tiện lợi hơn cho trường hợp cụ thể này.

Khi nào nên dùng useCallback?

  1. Truyền callback cho component con được tối ưu hóa: Đây là trường hợp sử dụng phổ biến và quan trọng nhất. Khi bạn có một component con được bọc trong React.memo và bạn cần truyền một hàm xử lý sự kiện (như onClick, onSubmit) xuống cho nó, hãy dùng useCallback. Điều này ngăn việc tạo ra hàm mới mỗi lần render, giúp React.memo hoạt động hiệu quả.
  2. Làm phụ thuộc cho các hook khác: Khi một hàm được sử dụng bên trong useEffect, useLayoutEffect, hoặc thậm chí là useMemo, việc bọc nó trong useCallback sẽ giúp tránh các vòng lặp vô hạn hoặc các hiệu ứng chạy lại không cần thiết.

Ví dụ thực tế:

const ParentComponent = () => {
  const [count, setCount] = useState(0)

  // Mỗi lần ParentComponent re-render, hàm này sẽ được tạo mới
  // const handleIncrement = () => console.log("Increment!");

  // Nhờ useCallback, handleIncrement chỉ được tạo 1 lần duy nhất
  // và tham chiếu của nó không đổi giữa các lần render.
  const handleIncrement = useCallback(() => {
    console.log('Increment!')
  }, []) // Mảng phụ thuộc rỗng vì hàm không phụ thuộc vào giá trị nào

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Re-render Parent</button>
      {/* ChildComponent sẽ không bị re-render một cách oan uổng nữa */}
      <ChildComponent onIncrement={handleIncrement} />
    </div>
  )
}

// React.memo sẽ so sánh nông các props, nếu props không đổi, nó sẽ không re-render
const ChildComponent = React.memo(({ onIncrement }) => {
  console.log('Child re-rendered!')
  return <button onClick={onIncrement}>Increment Button</button>
})

So sánh trực diện: useCallback vs. useMemo

Cú pháp của useCallback và useMemo

Bảng so sánh nhanh theo các tiêu chí quan trọng nhất

Tiêu chíuseMemouseCallback
Mục đíchGhi nhớ một giá trị (kết quả của hàm)Ghi nhớ một hàm
Trả vềGiá trị được ghi nhớ (string, number, object, array...)Hàm được ghi nhớ
Tương tự như"Hãy nhớ đáp án của bài toán này""Hãy nhớ cách giải của bài toán này"
Trường hợp dùngTính toán nặng, tạo đối tượng/mảng ổn địnhTruyền callback cho component con, phụ thuộc của hook

Cạm bẫy cần tránh: Khi nào KHÔNG nên dùng? ❌

Mặc dù rất mạnh mẽ, việc lạm dụng useCallbackuseMemo có thể phản tác dụng. Hãy nhớ rằng: Tối ưu hóa cũng có chi phí của nó.

Bản thân các hook này cũng cần bộ nhớ để lưu trữ giá trị/hàm và thời gian để so sánh mảng phụ thuộc.

Nguyên tắc vàng:

  1. Đừng tối ưu hóa sớm: Đừng bọc mọi thứ trong useCallback/useMemo một cách mù quáng.
  2. Đo lường trước khi hành động: Hãy dùng các công cụ như React DevTools Profiler để xác định chính xác những component nào đang bị re-render không cần thiết và gây ra tắc nghẽn hiệu năng.
  3. Ưu tiên hàng đầu: Trường hợp sử dụng rõ ràng và mang lại lợi ích lớn nhất là khi truyền props (hàm hoặc đối tượng) cho các component con được bọc trong React.memo.

Kết luận: Bộ ba hoàn hảo React.memo, useCallback, useMemo

Hãy xem chúng như một đội. React.memo là người lính gác cổng, ngăn chặn các re-render không cần thiết. Nhưng người lính này chỉ hiệu quả khi useCallbackuseMemo cung cấp cho nó những "giấy thông hành" (props) ổn định, không thay đổi tham chiếu sau mỗi lần render.

  • Dùng useMemo để ghi nhớ giá trị.
  • Dùng useCallback để ghi nhớ hàm.
  • Cả hai đều phục vụ mục đích duy trì sự ổn định tham chiếu (referential equality), ngăn chặn các re-render oan uổng.

Hiểu và sử dụng thành thạo bộ đôi này không chỉ giúp ứng dụng của bạn nhanh hơn mà còn thể hiện bạn là một lập trình viên React có tư duy sâu sắc về hiệu năng. Chúc bạn code vui! 🚀

Bài viết liên quan

[React Basics] React Context: Khái niệm & Cách sử dụng hiệu quả nhất

React Context là gì? Học cách sử dụng React Context API để quản lý state toàn cục dễ dàng và hiệu quả, loại bỏ Prop Drilling và tối ưu hóa ứng dụng React.

[React Basics] useState Hook: Cách quản lý State hiệu quả trong React

Hướng dẫn cách sử dụng useState Hook để quản lý state trong các function component của React. Tìm hiểu các ví dụ thực tế, từ cơ bản đến nâng cao, giúp bạn làm chủ React Hook quan trọng này.

[React Basics] React Handling Events: Những cách làm hiệu quả và tối ưu

Handling Events là một kỹ năng quan trọng trong React. Bài viết này sẽ giúp bạn hiểu rõ về cú pháp, cách truyền đối số và quản lý trạng thái khi làm việc với các sự kiện.

[React Basics] Thuộc tính key trong React: Hiểu rõ và sử dụng hiệu quả

Bạn đã thực sự hiểu về thuộc tính key trong React? Tìm hiểu vai trò, cách dùng hiệu quả và các ví dụ thực tế giúp code của bạn sạch và tối ưu hơn.