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í đó.
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 useCallback
và useMemo
, 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:
- 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).
- 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.
- 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:
- 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.
- 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êmReact.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!