Người dùng thích các trang web có UI nhanh và nhạy. Độ trễ phản hồi thấp dưới 100 mili giây tạo sẽ tạo cảm giác tức thì. Nếu độ trễ từ 100 – 300 mili giây sẽ làm người dùng cảm nhận rõ rệt về sự chậm chạp trang web.

Để cải thiện hiệu suất của web app, React cung cấp một HOC là React.memo().  Khi React.memo() bao quanh một component, React sẽ ghi nhớ kết quả render và bỏ qua các quá trình render không cần thiết.

Bài này mình sẽ giới thiệu về kĩ thuật memoization trong React, cụ thể là cách dùng React.memo() để cải thiện performance, cũng như các trường hợp nên dùng và không cần dùng React.memo().

1. React.memo()

Trước khi quyết định cập nhật DOM thật, React sẽ render component, sau đó so sánh kết quả với lần render trước đó. Nếu cho ra kết quả khác nhau, React sẽ cập nhật lại DOM thật.

Công việc render ra VDOM và so sánh giữa 2 VDOM khá là nhanh. Nhưng bạn còn có thể tăng tốc web app bằng cách bỏ qua công đoạn so sánh này nếu chúng không cần thiết.

Khi một component được bao gói bởi React.memo(), React render component và ghi nhớ kết quả. Trước khi đến lần render kế tiếp, nếu prop mới thì giống prop cũ, React sẽ sử dụng lại kết quả đã được ghi nhớ trước đó mà bỏ qua quá trình render.

Cùng xem cách hoạt động của việc ghi nhớ. Functional Component Movie được bao gói bởi React.memo()

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}

export const MemoizedMovie = React.memo(Movie);

React.memo(Movie) return một component được ghi nhớ mới có tên là MemoizedMovie. Nó sẽ cho ra nội dung y hệt component Movie nhưng có một khác biệt nho nhỏ.

Kết quả render của MemoizedMovie được ghi nhớ. Nội dung ghi nhớ này sẽ được sử dụng lại miễn là titlereleaseDate prop không thay đổi ở các lần render tiếp theo

// Lần đầu, React gọi component MemoizedMovie và render
<MemoizedMovie 
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

// Các lần tiếp theo, props vẫn không thay đổi
// React sẽ gọi MemoizedMovie và lấy kết quả của lần render đầu 
// Không cần render lại nữa
<MemoizedMovie
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

Mở console lên, bạn sẽ thấy React render <MemoizedMovie> chỉ một lần, trong khi đó <Movie> re-render lại rất nhiều lần.

Click vào đây nếu trình duyệt không hiển thị codesandbox


React.memo() được thiết kế riêng cho functional component, tương tự như PureComponent cho class component vậy

1.1 Custom việc so sánh các prop

Mặc định React.memo() thực hiện so sánh nông (shallow compare) các prop.

Bạn có thể sử dụng tham số thứ 2 để quyết định việc so sánh thay cho việc so sánh mặc định của React.memo

React.memo(Component, [areEqual(prevProps, nextProps)]);

Hàm areEqual(prevProps, nextProps) sẽ return true nếu prevPropsnextProps bằng nhau

Ví dụ cách tính bằng nhau như thế này

function moviePropsAreEqual(prevMovie, nextMovie) {
  return prevMovie.title === nextMovie.title
    && prevMovie.releaseDate === nextMovie.releaseDate;
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);

Hàm moviePropsAreEqual() return true nếu prevMovienextMovie bằng nhau.

2. Khi nào nên dùng React.memo()

Trước hết thì component của bạn phải là functional component đã nhé.

  • Nếu component của bạn luôn luôn bị re-render mặc dù prop không thay đổi
  • Component của bạn chứa một lượng lớn tính toán logic và UI như Chart, Canvas, 3D library…

Cá nhân mình sẽ dùng React.memo() là mặc định luôn khi code React-Hook.

2.1 Component re-render thường xuyên với cùng prop giống nhau

Một trường hợp cực kì phổ biến khi code React như sau. MovieViewsRealtime hiển thị số views của một bộ phim, với dữ liệu cập nhật realtime.

function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <Movie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}

App sẽ kết nối với server và cập nhật prop views

// Initial render
<MovieViewsRealtime 
  views={0} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>

// After 1 second, views is 10
<MovieViewsRealtime 
  views={10} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>

// After 2 seconds, views is 25
<MovieViewsRealtime 
  views={25} 
  title="Forrest Gump" 
  releaseDate="June 23, 1994" 
/>

Mỗi lần prop views được cập nhật với một con số mới, MovieViewsRealtime sẽ re-render. Điều này làm cho Movie cũng re-reder theo mặc dù titlereleaseDate không thay đổi.

Trường hợp này chúng ta sẽ áp dụng React.memo() để hạn chế việc re-render trên Movie component

function MemoizedMovie = React.memo(Movie)
function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  )
}

Miễn là titlereleaseDate không thay đổi, React sẽ bỏ qua quá trình re-render MemoizedMovie. Việc này sẽ giúp cải thiện hiệu suất của MovieViewsRealtime component.

3. Khi nào tránh sử dụng React.memo()

Như mình đã nói ở trên thì mình sẽ dùng React.memo() hầu hết mọi trường hợp khi sài React-Hook. Vậy những trường hợp nào bạn không nên sài React.memo()

  • Component của bạn là class component
  • Component của bạn đã được memo bởi một HOC khác, ví dụ connect() của Redux.

Có một số người cho rằng props component của họ thay đổi thường xuyên, nên họ không cần dùng React.memo(). Cũng đúng, nhưng theo ý kiến cá nhân của mình khi một code trong một  app React lớn, thì việc re-render component của bạn còn bị ảnh hưởng bởi các state của component cha, không chỉ là mỗi props các prop mà component con nhận vào. Nên chắc chắn sẽ có những lần re-render vô ích, không ít thì nhiều. Nên mình sẽ dùng React.memo() luôn cho đảm bảo.

 4. React.memo() và callback function

Các bạn chưa hiểu callback là gì thì có thể đọc bài này của mình nhá. Chinh phục High Order Function, Closures, Currying và Callback trong Javascript

Bây giờ chúng ta sẽ đi qua ví dụ:

Component Logout nhận một callback prop là onLogout, và được bao gói bởi React.memo():

function Logout({ username, onLogout }) {
  return (
    <div onClick={onLogout}>
      Logout {username}
    </div>
  );
}

const MemoizedLogout = React.memo(Logout);

Một component nhận một callback nên được xử lý với kĩ thuật memo. Mỗi lần component cha re-render nó sẽ tạo ra một instance callback mới.

function MyApp({ store, cookies }) {
  return (
    <div className="main">
      <header>
        <MemoizedLogout
          username={store.username}
          onLogout={() => cookies.clear('session')}
        />
      </header>
      {store.content}
    </div>
  );
}

Lúc này mặc dù username không thay đổi, MemoizedLogout vẫn render lại vì nó nhận một instance mới của onLogout callback.

React.memo() lúc này không có tác dụng nữa rồi.

Để fix điều này, ta dùng một hook của React đó là useCallback() để ngăn việc tạo instance callback mới giữa mỗi lần render.

const MemoizedLogout = React.memo(Logout);

function MyApp({ store, cookies }) {
  const onLogout = useCallback(
    () => cookies.clear('session'), 
    [cookies]
  );
  return (
    <div className="main">
      <header>
        <MemoizedLogout
          username={store.username}
          onLogout={onLogout}
        />
      </header>
      {store.content}
    </div>
  );
}

useCallback(() => cookies.clear('session'), [cookies]) luôn luôn return một instance function, miễn là cookies không thay đổi. Lúc này chúng ta đã fix được vấn đề trên.

5. Tóm lại

React.memo() là một công cụ tuyệt vời để ghi nhớ functional component. Khi dùng chính xác nó sẽ giúp bạn tăng performance và UX lên đáng kể đấy. Hãy cảnh giác với callback function nữa nhé, luôn luôn đảm bảo rằng instance của callback luôn giống nhau giữa các lần re-render.

Bạn còn biết trường nào nên dùng React.memo() nữa không? Hãy comment phía dưới cho mình biết với nhé!

Tham khảo: https://dmitripavlutin.com/use-react-memo-wisely/