[React Basics] Hiểu đúng về React.memo: Khi nào dùng và khi nào không?

Chắc hẳn bạn đã từng nghe câu "đừng tối ưu hóa sớm". Nhưng trong thế giới của React, nơi mỗi lần re-render không cần thiết đều có thể bào mòn hiệu năng và trải nghiệm người dùng, việc nắm vững các công cụ tối ưu hóa không phải là "sớm", mà là "thiết yếu". Và React.memo chính là một trong những vũ khí lợi hại nhất trong kho vũ khí đó.

Hiểu đúng về React.memo: Khi nào dùng và khi nào không?

Bài viết này sẽ đưa bạn đi từ những khái niệm cơ bản nhất đến các kỹ thuật nâng cao, giúp bạn làm chủ React.memo và biến nó thành trợ thủ đắc lực trong việc xây dựng các ứng dụng React nhanh như chớp. ⚡

Vấn đề muôn thuở: Re-render không cần thiết

Để hiểu tại sao React.memo lại quan trọng, trước tiên chúng ta cần hiểu cơ chế render mặc định của React. Khi state hoặc props của một component cha thay đổi, React sẽ render lại chính nó và toàn bộ các component con bên trong nó, bất kể props của các component con đó có thay đổi hay không.

Hãy xem xét ví dụ đơn giản sau:

import React, { useState } from 'react'

// Component con hiển thị tên người dùng
const UserProfile = ({ name }) => {
  console.log(`Rendering UserProfile với tên: ${name}`)
  return <div>Tên người dùng: {name}</div>
}

// Component cha
const App = () => {
  const [count, setCount] = useState(0)

  console.log('Rendering App...')

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me: {count}</button>
      <hr />
      {/* UserProfile không hề liên quan đến "count" */}
      <UserProfile name="Alex" />
    </div>
  )
}

export default App

Khi bạn chạy ứng dụng này và bấm vào nút, bạn sẽ thấy trong console:

Rendering App...
Rendering UserProfile với tên: Alex

Mỗi lần click, App re-render và UserProfile cũng bị re-render theo, dù cho prop name của nó không hề thay đổi "Alex". Trong một ứng dụng nhỏ, điều này không đáng kể. Nhưng hãy tưởng tượng UserProfile là một component phức tạp với nhiều logic và tính toán, và nó nằm trong một cây component khổng lồ. Việc re-render không cần thiết này sẽ trở thành một "cổ chai" hiệu năng nghiêm trọng.

Đây chính là lúc React.memo bước ra và tỏa sáng.

React.memo: "Người bảo vệ" cho Component của bạn

React.memo là một Higher-Order Component (HOC). Hiểu đơn giản, nó là một hàm "bọc" lấy một component và trả về một phiên bản được tối ưu hóa.

Memoization là một kỹ thuật tối ưu hóa bằng cách lưu lại (caching) kết quả của các phép tính tốn kém và trả về kết quả đã lưu khi gặp lại cùng một đầu vào. React.memo áp dụng nguyên lý này cho việc render component.

Phiên bản được "memoized" này sẽ chỉ re-render khi props của nó thay đổi.

Cách sử dụng cơ bản

Cú pháp của React.memo vô cùng đơn giản. Bạn chỉ cần bọc component của mình trong React.memo():

import React from 'react'

const MyComponent = (props) => {
  /* logic render */
}

// Xuất ra phiên bản đã được memoized
export default React.memo(MyComponent)

Bây giờ, hãy áp dụng nó vào ví dụ UserProfile ở trên:

import React, { useState } from 'react'

// Component con được bọc bởi React.memo
const UserProfile = React.memo(({ name }) => {
  console.log(`Rendering UserProfile với tên: ${name}`)
  return <div>Tên người dùng: {name}</div>
})

// Component cha không thay đổi
const App = () => {
  const [count, setCount] = useState(0)
  console.log('Rendering App...')
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me: {count}</button>
      <hr />
      <UserProfile name="Alex" />
    </div>
  )
}

Kết quả bây giờ?

  • Lần render đầu tiên:
    Rendering App...
    Rendering UserProfile với tên: Alex
    
  • Mỗi lần click tiếp theo:
    Rendering App...
    

UserProfile không còn bị re-render nữa! React.memo đã so sánh props của lần render trước { name: 'Alex' } và props của lần render mới { name: 'Alex' }, thấy chúng không thay đổi, và quyết định bỏ qua việc re-render, sử dụng lại kết quả render trước đó.

So sánh nông (Shallow Comparison) - Gót chân Achilles

Mặc định, React.memo thực hiện một phép so sánh nông (shallow comparison) trên đối tượng props. Điều này có nghĩa là nó chỉ so sánh các giá trị ở cấp đầu tiên của props.

  • Hoạt động tốt với: Các kiểu dữ liệu nguyên thủy (primitive types) như string, number, boolean, null, undefined.
  • Gặp vấn đề với: Các kiểu dữ liệu tham chiếu (reference types) như object, array, và đặc biệt là function.

Tại sao lại có vấn đề? Bởi vì khi so sánh các kiểu dữ liệu tham chiếu, JavaScript so sánh tham chiếu (địa chỉ trong bộ nhớ) chứ không phải giá trị thực tế bên trong.

Hãy xem ví dụ "kinh điển" sau:

//...
const MemoizedButton = React.memo(({ onClick }) => {
  console.log('Rendering Button')
  return <button onClick={onClick}>Nút Memoized</button>
})

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

  // Vấn đề ở đây!
  // Mỗi lần App re-render, một hàm "log" MỚI được tạo ra
  const logMessage = () => {
    console.log('Button clicked!')
  }

  return (
    <div>
      {/* ... */}
      <MemoizedButton onClick={logMessage} />
    </div>
  )
}

Dù đã dùng React.memo cho MemoizedButton, nó vẫn sẽ bị re-render mỗi khi App re-render. Tại sao? Vì mỗi lần App chạy lại, một hàm logMessage hoàn toàn mới được tạo ra. Mặc dù code bên trong giống hệt nhau, nhưng tham chiếu của chúng trong bộ nhớ lại khác nhau. React.memo so sánh prevProps.onClick !== nextProps.onClick và thấy chúng khác nhau, nên nó cho phép re-render.

Đây là lúc các "cặp bài trùng" của React.memo xuất hiện.

Cặp đôi hoàn hảo: useCallback và useMemo

Để giải quyết vấn đề với các kiểu dữ liệu tham chiếu, chúng ta cần đảm bảo rằng chúng ta truyền cùng một tham chiếu qua các lần render (trừ khi chúng thực sự cần thay đổi).

useCallback: Memoize hàm của bạn

Hook useCallback được sinh ra để giải quyết chính xác vấn đề với các hàm callback. Nó sẽ trả về một phiên bản được "memoized" của hàm callback, và phiên bản này chỉ thay đổi khi một trong các dependencies (phụ thuộc) của nó thay đổi.

Hãy sửa lại ví dụ trên với useCallback:

import React, { useState, useCallback } from 'react'

// ... MemoizedButton component

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

  // Sử dụng useCallback để memoize hàm logMessage
  // Mảng dependencies rỗng [] nghĩa là hàm này sẽ chỉ được tạo một lần duy nhất
  const logMessage = useCallback(() => {
    console.log('Button clicked!')
  }, []) // <-- Mảng dependencies

  return (
    <div>
      {/* ... */}
      <MemoizedButton onClick={logMessage} />
    </div>
  )
}

Bây giờ, MemoizedButton sẽ chỉ re-render khi logMessage thực sự thay đổi (trong trường hợp này là không bao giờ, vì mảng dependencies rỗng). Vấn đề đã được giải quyết!

useMemo: Memoize giá trị của bạn

Tương tự useCallback, nhưng useMemo dùng để memoize một giá trị (thường là kết quả của một phép tính tốn kém, hoặc một object/array).

import React, { useState, useMemo } from 'react'

const UserDetails = React.memo(({ user }) => {
  console.log('Rendering UserDetails')
  return (
    <div>
      {user.name} - {user.age}
    </div>
  )
})

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

  // Vấn đề: Mỗi lần re-render, một object user MỚI được tạo ra
  // const user = { name: 'Alex', age: 1 };

  // Giải pháp: Dùng useMemo
  const user = useMemo(
    () => ({
      name: 'Alex',
      age: 1,
    }),
    [],
  ) // Mảng dependencies rỗng, object này chỉ được tạo 1 lần

  return (
    <div>
      {/* ... */}
      <UserDetails user={user} />
    </div>
  )
}

Bằng cách kết hợp React.memo với useCallbackuseMemo, bạn có thể kiểm soát chính xác việc re-render và ngăn chặn hầu hết các trường hợp không cần thiết.

Khi shallow comparison không đủ: Tùy chỉnh logic so sánh

Trong một số trường hợp phức tạp, phép so sánh nông mặc định là không đủ. React.memo cung cấp một "cửa hậu" cho chúng ta: một tham số thứ hai tùy chọn, là một hàm so sánh.

React.memo(Component, areEqual(prevProps, nextProps))

Hàm areEqual này sẽ nhận vào props cũ và props mới. Nó phải trả về:

  • true: Nếu props được coi là bằng nhau, component sẽ KHÔNG re-render.
  • false: Nếu props được coi là khác nhau, component sẽ re-render.

Lưu ý: Logic này ngược với shouldComponentUpdate trong class component (trả về false để không re-render).

const UserCard = ({ user }) => {
  // ...
}

const areUserPropsEqual = (prevProps, nextProps) => {
  // Chỉ re-render nếu user.id thay đổi, bỏ qua các thay đổi khác
  return prevProps.user.id === nextProps.user.id
}

export default React.memo(UserCard, areUserPropsEqual)

Đây là một công cụ mạnh mẽ, nhưng hãy sử dụng nó một cách cẩn trọng. Một logic so sánh phức tạp có thể tốn kém hơn cả việc re-render component.

Tổng kết: Khi nào nên và không nên dùng React.memo?

React.memo không phải là viên đạn bạc. Việc bọc mọi component trong React.memo có thể gây tác dụng ngược do chi phí của việc so sánh props. Hãy là một lập trình viên thông thái.

✅ Hãy sử dụng React.memo khi:

  1. Component "thuần túy" (Pure Component): Component luôn trả về cùng một output với cùng một input (props).
  2. Render thường xuyên với cùng props: Component bị re-render nhiều lần nhưng props của nó lại ít khi thay đổi.
  3. Component nặng về tính toán: Logic render của component rất phức tạp và tốn kém, việc bỏ qua một lần re-render sẽ mang lại lợi ích lớn về hiệu năng.

❌ Cân nhắc kỹ hoặc không dùng React.memo khi:

  1. Props hầu như luôn thay đổi: Nếu props của component gần như luôn khác nhau giữa các lần render, việc so sánh sẽ trở nên thừa thãi và chỉ làm chậm ứng dụng của bạn.
  2. Component quá đơn giản: Với các component nhẹ (ví dụ: một thẻ div với vài thuộc tính), chi phí re-render là không đáng kể. Việc thêm React.memo có thể làm phức tạp hóa code mà không mang lại lợi ích rõ rệt.

Lời khuyên vàng: Luôn luôn đo lường hiệu năng (profile) ứng dụng của bạn trước và sau khi tối ưu hóa. Các công cụ như React DevTools Profiler sẽ cho bạn biết chính xác component nào đang gây ra vấn đề và liệu React.memo có thực sự hiệu quả hay không.

Chúc bạn thành công trên con đường chinh phục hiệu năng React!

Bài viết liên quan

[React Basics] Props trong React: Khái niệm, cách dùng và ví dụ thực tế

Giải mã props trong ReactJS: từ khái niệm cơ bản đến cách truyền dữ liệu giữa các component một cách hiệu quả. Khám phá các ví dụ minh họa dễ hiểu để bạn áp dụng ngay.

[React Basics] State trong React: Khái niệm, cách sử dụng và ví dụ chi tiết

Tìm hiểu khái niệm, cách khai báo và sử dụng State để quản lý dữ liệu động trong ứng dụng React của bạn. Xem ngay hướng dẫn chi tiết kèm ví dụ.

[React Basics] Hiểu rõ cách dùng useRef Hook trong React với các ví dụ thực tế

useRef hook là gì? Tìm hiểu cách sử dụng useRef trong React để tương tác với DOM, lưu trữ giá trị mà không gây re-render. Kèm theo ví dụ code chi tiết và dễ hiểu.

[React Basics] Hướng dẫn thiết lập môi trường phát triển React

Hướng dẫn cách thiết lập môi trường phát triển React cho người mới bắt đầu. Tìm hiểu các bước cài đặt Node.js, npm, và tạo project React đầu tiên với Create React App hoặc Vite.