React Memoization Visualized: useMemo, useCallback và React.memo

Hoang Vu,5 min read

Ngày 30 tháng 3, 2026

Memoization trong React không phải là "bật lên là nhanh". Nó là bài toán trade-off giữa chi phí so sánh, chi phí cache và chi phí re-render.

Bài này đi theo hướng trực quan để trả lời 3 câu hỏi:

  1. React.memo chặn re-render theo điều kiện nào?
  2. useCallback giúp gì cho React.memo?
  3. useMemo thực sự cache cái gì và khi nào invalid?

REACT MEMOIZATION DECISION MAP

Problem

problem

React.memo

memo

useCallback

callback

useMemo

memoValue

Result

result

Core idea

Parent re-renders propagate to children and expensive calculations.

Dependency contract

Caching only works when dependency arrays are accurate and prop references are stable.

Practical rule

Apply memoization only to proven hot paths, not everywhere by default.


1. Vấn đề gốc: vì sao component re-render quá nhiều?

Trong React, khi parent render lại, child cũng thường render lại theo cây.

Điều này không luôn xấu, nhưng sẽ tốn tài nguyên khi:

  1. child nặng,
  2. props giữ nguyên,
  3. hoặc có phép tính đắt đỏ lặp đi lặp lại.

Memoization giúp React "bỏ qua công việc không cần thiết" trong các trường hợp đó.


2. React.memo: chặn render nếu props không đổi

React.memo(Component) sẽ shallow compare props cũ/mới.

Nếu props bằng nhau theo shallow equality, React có thể skip render component đó.

const UserCard = React.memo(function UserCard({ user }) {
  return <div>{user.name}</div>;
});

Lưu ý quan trọng:

  1. React.memo không phải "never render again".
  2. Nó chỉ skip khi props của chính component đó không đổi.
  3. Nếu props là object/function mới mỗi lần render, memo khó phát huy hiệu quả.

2.1 React so sánh props bằng gì?

Mặc định, React.memo làm shallow compare từng prop bằng logic gần với Object.is.

Hiểu nhanh:

  1. Primitive (number, string, boolean, null, undefined) so sánh theo giá trị.
  2. Object/array/function so sánh theo reference, không so sánh sâu nội dung.

Vì vậy:

  1. count={1} qua nhiều render vẫn dễ được coi là "không đổi".
  2. {a: 1} tạo mới mỗi render là reference mới, nên bị coi là "đã đổi" dù nội dung giống nhau.
  3. () => doSomething() tạo mới mỗi render cũng là function reference mới.

Ví dụ gây fail memo:

<Child config={{ pageSize: 20 }} onSave={() => save(id)} />

pageSize và logic save không đổi, child vẫn có thể re-render vì cả configonSave đều là reference mới.

2.2 Cách ổn định object/function prop

  1. Dùng useMemo cho object/array prop.
  2. Dùng useCallback cho function prop.
const config = useMemo(() => ({ pageSize: 20 }), []);
const onSave = useCallback(() => save(id), [id]);
 
return <Child config={config} onSave={onSave} />;

2.3 Hàm comparator custom trong React.memo (areEqual / isEqual)

React.memo cho phép truyền comparator custom làm tham số thứ hai:

const Child = React.memo(
  function Child(props) {
    return <div>{props.user.name}</div>;
  },
  (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id;
  },
);

Quy tắc quan trọng (dễ nhầm):

  1. Comparator trả về true => React bỏ qua render.
  2. Comparator trả về false => React render lại.

Bạn có thể nghe tên isEqual, areEqual, hoặc dùng thư viện deep compare như lodash/isEqual, nhưng cần rất cẩn thận:

  1. Deep compare có chi phí CPU, có thể đắt hơn chính render.
  2. Comparator sai logic dễ làm UI stale (không render khi đáng ra phải render).
  3. Nếu comparator bỏ qua function prop, có thể giữ callback cũ và dính stale closure.

Thực dụng nhất:

  1. Ưu tiên ổn định reference bằng useMemo/useCallback trước.
  2. Chỉ dùng comparator custom cho hotspot đã profile rõ.
  3. Giữ comparator ngắn, dễ chứng minh tính đúng.

3. useCallback: giữ ổn định reference của function prop

React.memo thường "thất bại" vì function prop bị tạo mới mỗi lần parent render.

const onSave = () => save(id); // function mới ở mỗi render

Khi đó child nhận prop mới, shallow-compare fail, child render lại.

useCallback giúp giữ identity function ổn định theo dependency:

const onSave = useCallback(() => save(id), [id]);

USECALLBACK + REACT.MEMO IN PRACTICE

parent tick: 0

actions: 0

Inline callback (unstable reference)

memo child with inline callback

Child render count: 1

useCallback (stable reference)

memo child with stable callback

Child render count: 1

Parent re-renders change inline function identity, which breaks memo skip. useCallback keeps identity stable.

Checklist nhanh:

  1. Có truyền callback xuống child memoized không?
  2. Parent có re-render vì state unrelated không?
  3. Callback đó có thể ổn định reference bằng useCallback không?

4. useMemo: cache giá trị tính toán đắt đỏ

useMemo memoize kết quả của một hàm tính toán.

const filtered = useMemo(() => {
  return products.filter((p) => p.name.includes(keyword));
}, [products, keyword]);

Nó hữu ích khi:

  1. phép tính có chi phí đáng kể,
  2. dependency ít đổi,
  3. cùng giá trị được tái sử dụng qua nhiều render.

USEMEMO CACHE VS RE-RENDER NOISE

Without useMemo

Compute count: 1

React, Redux, Recoil, Relay, Remix

With useMemo

Compute count: 1

React, Redux, Recoil, Relay, Remix

Toggle unrelated state to see that useMemo keeps cached value until keyword dependency changes.

Điểm dễ nhầm:

  1. useMemo không đảm bảo performance trong mọi tình huống.
  2. Tự nó cũng có chi phí quản lý dependency + cache.
  3. Khi dependency đổi, cache bị invalidation.
  4. Dùng tràn lan có thể làm code khó đọc hơn lợi ích thực tế.

5. Kết hợp 3 công cụ đúng cách

Mô hình thường gặp trong production:

  1. Child nặng được bọc React.memo.
  2. Callback truyền xuống child dùng useCallback.
  3. Dữ liệu derived list/map dùng useMemo.

Ví dụ:

const Row = React.memo(function Row({ item, onSelect }) {
  return <button onClick={() => onSelect(item.id)}>{item.name}</button>;
});
 
function ProductList({ products, keyword }) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.includes(keyword)),
    [products, keyword],
  );
 
  const handleSelect = useCallback((id: string) => {
    console.log("select", id);
  }, []);
 
  return (
    <div>
      {filtered.map((item) => (
        <Row key={item.id} item={item} onSelect={handleSelect} />
      ))}
    </div>
  );
}

6. Khi nào KHÔNG nên memoize?

  1. Component nhỏ, render nhanh, tần suất thấp.
  2. Chưa có số liệu profiling.
  3. Dependency thay đổi liên tục khiến cache gần như vô dụng.
  4. Team phải trả giá lớn về độ phức tạp code.

Quy tắc thực dụng:

  1. Profile trước.
  2. Tối ưu đúng điểm nghẽn.
  3. Đo lại sau khi tối ưu.

Thuật ngữ nhanh (Glossary)

  1. shallow compare: so sánh ở level prop trực tiếp, không đi sâu object lồng nhau.
  2. reference: địa chỉ object/function trong bộ nhớ.
  3. identity: danh tính reference của một giá trị qua các lần render.
  4. stale: dữ liệu/UI cũ không phản ánh trạng thái mới nhất.
  5. stale closure: callback giữ biến từ render cũ.
  6. deep compare: so sánh sâu toàn bộ cấu trúc dữ liệu.
  7. invalidation: hủy cache cũ khi dependency đổi.
  8. hotspot: điểm nghẽn hiệu năng đáng ưu tiên.
  9. profile/profiling: đo hiệu năng để biết đúng điểm cần tối ưu.
  10. trade-off: đánh đổi chi phí/lợi ích khi tối ưu.

Tổng kết

React.memo, useCallback, useMemo là bộ ba cực mạnh nếu dùng đúng ngữ cảnh.

Hãy nhớ:

  1. React.memo tối ưu render của component.
  2. useCallback tối ưu identity của function prop.
  3. useMemo tối ưu kết quả tính toán.

Tối ưu hiệu năng trong React nên bắt đầu từ dữ liệu đo đạc, không bắt đầu từ cảm giác.

2026 © @hoag/blog.