React Profiler를 통한 렌더링 최적화

이수빈·2023년 1월 19일
2
post-thumbnail

React Profiler를 이용해 컴포넌트의 렌더링 속도를 측정하고 이를 최적화하기


SW 엘리스 2차 프로젝트에서 코드를 개선했던 점을 작성하게 되었다.


React 컴포넌트는 언제 리렌더링이 되는가?

  • 컴포넌트가 받는 Props가 변경되었을때
  • 사용하는 State가 변경되었을때
  • Context Value가 변경되었을때
  • 부모 컴포넌트가 리렌더링 되었을때

컴포넌트 렌더링 최적화 방법?

  • 리액트에서의 렌더링 최적화는 불필요한 리렌더링을 줄이는 방향으로 개선한다.
    부모컴포넌트가 리렌더링되었을때, 자식 컴포넌트가 prop으로 함수나 값을 받고있는 상황이라고 가정하자.

  • 여기서 prop값 자체가 변경되지 않았더라도 부모 컴포넌트가 리렌더링된다면
    전달하는 값이나 함수도 다시 렌더링이 되기 때문에 자식 컴포넌트 또한 불필요하게 렌더링 되는 상황이 발생한다.

  • 이를 방지하기 위해 React에서는 React.memo라는 고차컴포넌트와
    useCallback, useMemo와 같은 hook을 이용해 불필요한 렌더링을 막는다.


React.memo

const Comment = React.memo(function MyComponent(props) {
  /* prop이 변했을때만 Re-rendering을 실시함*/
});
export default React.memo(Comment); 
``` //이런 형태로도 사용.
  • React.memo는 컴포넌트 로직을 재사용하기 위한 고차컴포넌트이다.

  • React.memo를 통해 컴포넌트를 래핑하게 되면, 전달받는 prop이 변할때에만 컴포넌트를 리렌더링한다.
    만약 전달되는 prop으로 동일한 결과를 렌더링한다면, 컴포넌트를 리렌더링 하지 않고 메모이제이션 된 값을 사용한다


useCallback, useMemo

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 보통 값을 메모이제이션 할때는 useMemo, 함수를 메모이제이션 할때는 useCallback hook을 사용한다.
  • prop으로 전달되는 함수, 값들을 메모이제이션함으로써 React.memo로 래핑된 컴포넌트에 전달한다.
  • 실제값이 변경되지 않는 한 불필요한 리렌더링을 막을 수 있다.

React Profiler API

const FoodDetailProfiler = () => {
  return (
    <>
      <Profiler
        id="FoodDetail"
        onRender={(id, phase, actualTime, baseTime, startTime, commitTime, interactions) =>
          console.table({ id, phase, actualTime, baseTime, startTime, commitTime, interactions })
        }>
        <FoodDetail />
      </Profiler>
    </>
  );
};
export default FoodDetailProfiler;
  • 렌더링속도를 측정해보기위해 Profiler API를 각 컴포넌트에 감싸는 형태와 개발자도구의 Profiler Tab을 활용하였다.

  • Render함수의 파라미터들의 의미는 각각 다음과 같다.

    function onRenderCallback(
      id, // 방금 커밋 한 프로파일 러 트리의 "id"소품
      phase, // "mount"(트리가 방금 마운트 된 경우) 또는 "update"(다시 렌더링 된 경우)
      actualDuration, // 커밋 된 업데이트를 렌더링하는 데 소요 된 시간
      baseDuration, // 메모없이 전체 하위 트리를 렌더링하는 데 걸리는 예상 시간
      startTime, // React가 이 업데이트를 렌더링하기 시작했을 때
      commitTime, // React가 이 업데이트를 커밋했을 때
      interactions //이 업데이트에 속하는 상호 작용 집합
    )```

  • 여기서 actualDuration과 baseDuration 을 비교해 메모이제이션으로 얼마만큼의 시간을 절약하는지 알 수 있다.

  • actualDuration: memo, shouldComponentUpdate, useMemo 등과 같이 사용 중인 모든 최적화를 포함하여 구성 요소가 렌더링하는 데 걸린 실제 측정 시간이다.

  • baseDuration: baseDuration은 성능 개선 사항(메모이제이션)이 제거된 구성 요소를 렌더링하는 데 걸린 시간이다. 이 값은 메모이제이션을 사용하여 얼마나 많은 시간을 절약하고 있는지 알 수 있다.

개발자도구 Profiler Tab

  • 개발자도구의 ProfilerTab은 개발자도구에서 개별 컴포넌트의 성능측정을 쉽게 할 수 있도록 도와준다.
  • Flamegraph, Ranked Tab을 주로 사용한다.
  • Flamegraph Tab의 각 막대는 React 컴포넌트들을 나타내고, 각 막대의 너비는 컴포넌트와 해당 자식을 렌더링하는데 걸리는 시간을 알아 볼 수 있도록 색깔로 구분해져 있다. 전체 컴포넌트중 어떤것이 리렌더링 되고, 리렌더링 된 이유가 무엇인지도 알 수 있다.
  • Ranked Tab은 컴포넌트 자체에 소요된 시간만 다룬다. 세부 컴포넌트별 렌더링시간을 볼 때 유용하다.
  • 먼저 프로파일링 시작 버튼(파란색)을 클릭하면, 프로파일러는 각 컴포넌트들이 렌더링 된 시간들을 기록한다.
  • 기록이 시작되면, 컴포넌트들은 각 커밋을 생성한다. 모든 커밋을 확인하는 것은 최적화 작업에 번거로우므로, 프로파일러 설정값에서 일정 ms이하의 커밋들은 숨김처리 할 수 있고, 프로파일링 중 각 구성요소가 렌더링된 이유를 기록하는 것 또한 변경 할 수 있다.

최적화 진행 이전결과

  • 다음은 댓글을 삭제했을때 새로 리렌더링이 된 컴포넌트들을 측정한 프로파일링 결과이다.

  • 총 리렌더링 시간은 14.2ms가 걸렸다. 최적화 작업을 진행하지 않아서 부모 컴포넌트가 리렌더링되어서 자식 컴포넌트가 리렌더링 된 경우나 결과는 같지만, prop이 변경되었기 때문에 불필요하게 리렌더링 된 컴포넌트들이 존재한다.

  • 여기서 막대의 색은 현재 commit에서 컴포넌트를 렌더링하는데 얼마만큼의 시간이 걸렸는지 나타낸다.

  • 노란색 막대 : 상대적으로 더 많은 시간이 걸림

  • 파란색 막대 : 상대적으로 더 적은 시간이 걸림

  • 회색 막대 : 현재 commit에서 렌더링하지 않음

  • console에 찍힌 결과도 확인해보았다.

  • 메모이제이션을 안한 상태이므로, 댓글 하나를 삭제했을때 불필요하게 다른 컴포넌트들 또한 전부 re-rendering되는 상황이 발생한다. baseDuration과 actualDuration의 차이가 거의 없었다.

  • 이제 React.memo, useCallback, useMemo를 통해 최적화 작업을 진행해보았다.


최적화 진행 이후

  • 동일하게 댓글을 하나 삭제한 후 컴포넌트를 프로파일링 해보았다.

  • 총 렌더링 시간은 1.9ms로 최적화를 진행하지 않았을 때의 시간 14.2ms보다 약 86% 빨라졌다.

  • CommentList는 댓글을 담고있는 배열이므로, 댓글이 삭제되는 로직이 진행되었을때, FoodDetail 컴포넌트가 리렌더링 되면서, CommentList 컴포넌트만 리렌더링 되야한다.


  • Profiler 컴포넌트를 통해 정밀한 시간을 측정해보았다.

  • actualTime값이 0인것을 보아, 메모이제이션 된 값을 사용해 리렌더링이 되지 않았음을 알 수 있다.


ref) 공식문서참조

profile
응애 나 애기 개발자

0개의 댓글