React & Frontend8 phút đọc · 27 thg 5, 2026

Tối ưu performance trong React: re-render, memo, useMemo và useCallback

✦ Tóm tắt bởi AI

React mặc định đã nhanh, nhưng app vẫn có thể giật lag do render lại thừa. Phần lớn vấn đề performance không cần giải pháp phức tạp, chỉ cần hiểu re-render hoạt động thế nào rồi sửa đúng chỗ, bắt đầu từ việc tách component hợp lý, đặt state gần nơi dùng, rồi chỉ dùng React.memo, useMemo, useCallback khi thực sự đo được chúng cần thiết, vì tối ưu sớm thường phản tác dụng.

HOLETEX · POST
REACT
Tối ưu

React mặc định đã nhanh. Nhưng một app React vẫn có thể giật lag khi component render lại nhiều hơn cần thiết, hoặc mỗi lần render lại làm việc nặng không đáng. Tin tốt là phần lớn vấn đề performance không cần giải pháp cao siêu, chỉ cần hiểu re-render diễn ra thế nào rồi sửa đúng chỗ.

Bài này nói về tối ưu React theo hướng thực tế: re-render là gì và khi nào nó mới là vấn đề, dùng React.memo, useMemo, useCallback ra sao, và quan trọng không kém, khi nào KHÔNG nên dùng. Nếu bạn còn lạ với hooks, nên đọc trước React Hooks rồi quay lại đây.

Re-render trong React diễn ra thế nào

React hiển thị giao diện qua ba bước: trigger (kích hoạt một lần render), render (gọi component của bạn), và commit (ghi thay đổi lên DOM). "Render" ở đây chỉ đơn giản là React gọi function component để biết cần hiển thị gì, chứ chưa động vào DOM.

Có đúng hai lý do khiến một component render: lần đầu khi app khởi động, và mỗi khi state thay đổi. Tài liệu React nói rõ: gọi hàm set của useState sẽ tự động xếp hàng một lần render mới.

Điểm hay bị hiểu nhầm: khi một component render lại, React mặc định render lại toàn bộ cây component con bên trong nó, kể cả khi props của con không hề đổi. Đây không phải bug, đó là cách React hoạt động. Sau khi render, React so sánh kết quả với lần trước và chỉ sửa những DOM node thực sự khác nhau. Nói cách khác, "render lại" không đồng nghĩa với "cập nhật DOM", và bản thân nó thường rất nhanh.

Khi nào re-render mới là vấn đề

Đa số re-render là vô hại, đừng vội coi mọi lần render là kẻ thù. Render chỉ thành vấn đề khi gộp đủ hai yếu tố: component render rất thường xuyên (ví dụ gõ phím, kéo chuột, animation), và mỗi lần render lại làm việc nặng, như tính toán trên mảng lớn, hoặc kéo theo cả một cây con đồ sộ render theo.

Một mẫu hay gặp: bạn đặt một ô input ở component cha. Mỗi ký tự gõ vào làm cha render lại, kéo theo một bảng vài trăm dòng dưới nó render lại theo, dù bảng đó chẳng liên quan gì tới ô input. Lúc này tối ưu mới có ý nghĩa.

Cách xử lý đầu tiên, và thường là tốt nhất, không phải là memo mà là giữ state ở gần nơi dùng nó. Nếu chỉ ô input cần value, hãy tách ô input và state của nó thành một component riêng, để khi gõ chỉ component nhỏ đó render lại. Việc đặt state ở đâu là phần lõi của quản lý state trong React, và sửa được nhiều vấn đề performance trước khi cần tới bất kỳ hook tối ưu nào.

React.memo: bỏ qua render khi props không đổi

Khi đã tách component hợp lý mà một component con đắt đỏ vẫn render lại thừa, React.memo cho phép React bỏ qua render lại con nếu props của nó không đổi so với lần trước.

jsx
import { memo } from 'react';

const ProductRow = memo(function ProductRow({ name, price }) {
  // chỉ render lại khi name hoặc price đổi
  return (
    <tr>
      <td>{name}</td>
      <td>{price}</td>
    </tr>
  );
});

React so sánh từng prop bằng Object.is. Nếu tất cả props giống lần trước, nó tái dùng kết quả cũ và không gọi lại function. memo phát huy tác dụng nhất khi component render thường xuyên với cùng props, và việc render lại nó tốn kém.

Lưu ý quan trọng từ tài liệu React: chỉ nên xem memo như một bước tối ưu. Nếu code của bạn chỉ chạy đúng khi có memo, tức là đang có một bug ở đâu đó (thường là vi phạm tính thuần khiết của render), hãy tìm và sửa gốc trước.

Cạm bẫy lớn nhất: object và function mới mỗi render

React.memo chỉ giúp được nếu props thật sự giữ nguyên giữa các lần render. Mà trong JavaScript, mỗi object, array, hay function viết inline đều là một giá trị mới mỗi lần render, dù nội dung y hệt: {} !== {}() => {} !== () => {}.

jsx
// ❌ ProductRow đã memo nhưng vẫn render lại mỗi lần cha render
<ProductRow
  name={name}
  price={price}
  style={{ color: 'red' }}        // object mới mỗi render
  onSelect={() => select(id)}     // function mới mỗi render
/>

Hai prop styleonSelect luôn "mới", nên memo luôn thấy props khác và render lại như thường. Đây là lý do số một khiến memo "không có tác dụng".

Cách sửa đơn giản nhất thường là truyền giá trị nguyên thủy thay vì gói vào object, và đẩy những object hằng số ra ngoài component:

jsx
const rowStyle = { color: 'red' }; // tạo một lần, ngoài component

function Table() {
  // ...
  return <ProductRow name={name} price={price} style={rowStyle} />;
}

Còn với function thì cần useCallback, sẽ nói ngay dưới đây.

useMemo và useCallback: dùng khi nào, và khi nào KHÔNG

Hai hook này cùng giải quyết bài toán "giữ nguyên một giá trị giữa các lần render", chỉ khác đối tượng:

  • useMemo cache kết quả của một phép tính.
  • useCallback cache chính một function.

useMemo có hai công dụng. Một là bỏ qua một phép tính nặng khi đầu vào không đổi. Hai là giữ ổn định reference của object/array để truyền cho component đã memo.

jsx
import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  // chỉ lọc lại khi todos hoặc tab đổi
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return <List items={visibleTodos} />;
}

useCallback giữ nguyên reference của một function qua các lần render, để component con đã memo không bị render lại vì nhận "function mới":

jsx
import { useCallback } from 'react';

function ProductPage({ productId }) {
  const handleSubmit = useCallback((orderDetails) => {
    postOrder(productId, orderDetails);
  }, [productId]); // function chỉ đổi khi productId đổi

  return <ShippingForm onSubmit={handleSubmit} />;
}

Về mặt khái niệm, useCallback(fn, deps) chính là useMemo(() => fn, deps).

Đây là phần quan trọng nhất: đừng rải useMemo/useCallback khắp nơi. Tài liệu React nói thẳng rằng chúng chỉ thực sự có giá trị trong vài trường hợp:

  • Phép tính rõ ràng là chậm, và dependency của nó hiếm khi đổi.
  • Bạn truyền giá trị/function đó xuống một component đã bọc memo và muốn nó bỏ qua render thừa.
  • Giá trị đó được dùng làm dependency của một hook khác (như useEffect).

Ngoài các trường hợp đó, memo hóa thường không đem lại lợi ích đo được, mà còn làm code rườm rà khó đọc hơn, và bản thân việc kiểm tra cache cũng tốn một chút. Tối ưu sớm ở đây thường lỗ nhiều hơn lãi. Một useCallback không kèm React.memo ở phía con thường chẳng giúp gì cả.

Dùng key đúng cách khi render list

key không phải để tối ưu theo nghĩa thông thường, nhưng dùng sai key là một trong những nguyên nhân âm thầm khiến React làm việc thừa và sinh bug. Key giúp React nhận ra đâu là cùng một phần tử qua các lần render khi list bị thêm, xóa, hay sắp xếp lại.

jsx
{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

Hai điều cần tránh:

  • Đừng dùng index của mảng làm key khi list có thể đổi thứ tự, thêm hoặc xóa phần tử. Index không gắn với dữ liệu, nên khi list thay đổi React dễ khớp nhầm item, gây bug khó hiểu và render lại không cần thiết.
  • Đừng sinh key động kiểu key={Math.random()}. Key sẽ không bao giờ khớp giữa các lần render, khiến React tạo lại toàn bộ DOM mỗi lần, vừa chậm vừa mất state/input bên trong các item.

Hãy dùng một id ổn định gắn với dữ liệu (id từ database, hoặc crypto.randomUUID() khi tạo dữ liệu phía client).

Nhiều khi bạn không cần Effect, cũng không cần memo

Một nguồn render thừa lớn mà ít người để ý là lạm dụng useEffect để tính state. Nếu một giá trị có thể tính ra từ props hoặc state hiện có, hãy tính thẳng khi render, đừng nhét vào state rồi cập nhật trong Effect.

jsx
// ❌ thừa state + Effect, gây thêm một vòng render
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// ✅ tính thẳng khi render
const fullName = firstName + ' ' + lastName;

Cách thứ hai vừa ít code, vừa nhanh hơn vì tránh được vòng cập nhật "cascading". Nếu phép tính đó nặng thật, bọc nó bằng useMemo, chứ vẫn không cần đẩy vào state. Tránh được Effect thừa thường cải thiện performance rõ hơn là thêm memo vào cuối.

Tài liệu React còn nhắc một điểm nền tảng: hãy giữ component pure, nghĩa là render chỉ tính toán và trả về JSX, không sửa biến/object có từ trước. Chính vì component pure mà React mới dám bỏ qua render hay cache kết quả an toàn. Nếu phải dựa vào memo để app chạy đúng, gần như chắc chắn có một side effect lén lút trong lúc render cần sửa trước.

Đo trước khi tối ưu

Nguyên tắc xuyên suốt: đừng đoán, hãy đo. Trước khi thêm bất kỳ memo nào, hãy xác nhận chỗ đó thật sự chậm.

  • Dùng React DevTools Profiler để xem component nào render, render bao nhiêu lần, và mỗi lần tốn bao lâu. Đây là cách nhanh nhất để biết nên tối ưu ở đâu.
  • Đo một phép tính cụ thể bằng console.time:
jsx
console.time('filter');
const result = filterTodos(todos, tab);
console.timeEnd('filter');

Nếu tổng thời gian dưới khoảng 1ms thì gần như chắc chắn không cần useMemo. Lưu ý đo trên production build và bật CPU throttling để mô phỏng máy yếu, vì dev build luôn chậm hơn thực tế.

Cuối cùng, đáng biết là React Compiler đang dần tự động memo hóa component và giá trị, giảm nhu cầu viết tay memo/useMemo/useCallback. Nhưng hiểu re-render vẫn là kỹ năng cốt lõi: nó giúp bạn viết code sạch ngay từ đầu và biết khi nào một tối ưu là cần thiết.

Tóm lại, hãy đi theo thứ tự này: viết component pure và đặt state ở gần nơi dùng → tính derived state khi render thay vì qua Effect → đo bằng Profiler → và chỉ khi đó mới thêm memo/useMemo/useCallback đúng chỗ đo được. Tối ưu React tốt phần lớn là tránh việc thừa, không phải nhồi thêm hook.

Muốn viết React vừa nhanh vừa sạch? Hiểu sâu re-render và state là thứ tách dev "code chạy được" khỏi dev làm chủ React. Khóa React PRO của HoleTex dạy đúng những nền tảng này qua dự án thật, kèm review code, để bạn tối ưu vì đo được chứ không vì cảm tính.

Bài liên quan

Nguồn tham khảo: react.dev/learn/render-and-commit, react.dev/reference/react/memo, react.dev/reference/react/useMemo, react.dev/reference/react/useCallback, react.dev/learn/keeping-components-pure, react.dev/learn/you-might-not-need-an-effect, react.dev/learn/rendering-lists. Đã đối chiếu 2026-06-07.

Thấy hay? Chia sẻ