[회고] Chrome debugging tool의 performance 측정을 통해 UI 스레드 최적화하기

in-ch·2024년 4월 20일
1

헤딩 기록

목록 보기
15/18

intro


최근에 새로운 프로젝트로 Electron을 활용해 데스크탑 어플리케이션을 개발하고 있습니다.

이 프로젝트에서의 요구 사항 중 하나는 패널을 분리하고 사용자가 드래그하여 창의 크기를 조절할 수 있는 기능을 구현하는 것이였습니다.

이 gif를 보면 이해가 편합니다.

직접 구현하기 보다는 react-resizable-pannels 라이브러리를 활용하였는데요.
문제는 구현을 완료하고 스트레스 테스트를 하던 중 UI 스레드의 저하 문제가 발견되었습니다.

이 문제가 발생한 이유와 해결책을 찾은 과정, 그리고 어떻게 개선했는지에 대해 회고하겠습니다. 😃

문제 공유


react-resize-panels

문제 영상부터 보시죠.

위의 gif에서 보다시피 패널 사이즈를 마우스로 조절하는데 UI 스레드의 저하 문제로 프레임 드랍 현상이 발생하였습니다.

프레임 드랍이란?

프레임 드랍이란?

화면이 느려지거나 동영상의 버퍼, 프레임 레이트(프레임율) 저하 등 계산의 한계 처리 속도 이상의 처리가 올 경우 그 처리가 늦어져서 수직동기 신호를 놓쳐 화면의 갱신이 늦어지는 현상을 뜻합니다.

문제 측정하기

문제 해결을 위해 Chrome debug tool을 활용해서 performance 측정을 시작하였습니다.

메인 스레드를 확인해 봤을 때 다음 문구를 확인해 볼 수 있었습니다.

Waring Long task took 258.41 ms.

즉, 258.41 ms 정도 긴 작업이 있었다는 뜻입니다.
그럼 어떤 작업이 있었는지 확인해보죠.

Aggregated Time을 확인해봤을 때 대부분의 작업이 Rendering 작업이였던 것을 확인했습니다.

참고로 연보라색이 Rendering 작업을 뜻하는 거고 시간 순에 따라 보라색 그래프가 지속적으로 위로 치솟는 것을 확인해 볼 수 있습니다.

어떻게 해결할까?

가장 크게 개선할 수 있는 방법은 렌더링 작업을 간소화시키는 것입니다.

스트레스 테스트에서 Panel1 영역에 10,000개의 컴포넌트를 렌더링하게 하였는데요.
가장 좋은 방법은 렌더링되는 컴포넌트의 수를 줄이는 것입니다.

실제로 10,000에서 1,000개로 갯수를 줄였더니 유의미한 결과를 확인해 볼 수 있었습니다.

따라서 인피니티 스크롤링 등을 도입하여 렌더링되는 컴포넌트의 갯수를 최적화한다면 어느정도 문제를 해결할 수 있습니다.

더 완벽한 방법을 찾아보자.

위의 방법은 근본적인 해결 방법이 아니였습니다.
인피니티 스크롤링을 적용했다고 하더라도 유저가 엄청나게 많은 스크롤을 진행했다면 무용지물이 됩니다.

따라서 더 근원적인 문제를 해결하기 위해 더 자세히 살펴봤습니다.

눈에 띄는 점은 바로 다음과 같았습니다.

바로 가장 오랜 작업 시간이 소요되는 렌더링 작업이 패널 크기를 조절하는 과정에서 minSize 구간마다 발생하는 것이였습니다. (빨간색 색칠한 부분)

그렇다면 왜 발생했는지 확인해보죠.

Warning: Forced reflow is a likely performance bottleneck.

주된 원인은 아직 원인은 모르겠으나 빨간색으로 칠한 부분마다 Forced reflow 현상이 발생했다는 것입니다!

reflow란 무엇인가?

reflow를 이해하기 위해서는 먼저 브라우저의 렌더링 과정을 살펴봐야 합니다.

  • 파싱과 스타일링: HTML 문서를 파싱하고 CSS 스타일을 적용하여 요소들의 초기 위치와 스타일을 결정합니다.
  • 레이아웃(배치): 요소들의 크기와 위치를 계산하여 화면에 배치합니다. 이 단계에서는 각 요소의 실제 위치와 크기를 결정하고 화면에 표시될 공간을 확보합니다.
  • 페인트(그리기): 레이아웃이 결정된 후에는 각 요소를 화면에 그립니다. 이 과정에서는 각 요소의 스타일에 따라 텍스트, 이미지 등을 화면에 표시합니다.

여기서 주목할 점은 Layout입니다.

레이아웃 단계(layout)에서의 reflow(재배치)는 요소의 크기나 위치 등의 변경으로 인해 다시 레이아웃을 계산하는 과정을 의미합니다.

그리고 이 과정은 매우 많은 비용이 들어가게 됩니다.

reflow의 주요 발생 원인

  • viewport 변화 (윈도우 리사이징)
  • 스타일 추가
  • :hover 등 CSS Pseudo Class
  • class의 동적 변화
  • JS를 통한 DOM 동적 변화
    등 등 ...

즉, 다음들과 같은 원인들 하나 때문에 minSize의 구간마다 reflow가 발생하고 성능 저하 이슈가 발생한 겁니다.

하지만 위에서 언급했다시피 외부 라이브러리를 사용하였기 때문에 직접적인 문제 해결은 불가능했고 대신 이슈를 남겼습니다.

이슈 링크

문제 해결


문제 해결은 허무하기는 결국 라이브러리의 버전 문제였습니다.

실제로 버전을 v0.0.58에서 v2.0.17로 업그레이드 후 테스트해보니 문제가 해결된 것을 확인해 볼 수 있었습니다.

무엇이 바뀌었는지 확인해보기


정확하지 않을 수는 있지만 무엇이 변경되어 reflow를 해결했는지 궁금하여 확인해보았습니다.

관련 이슈

그러다 관련이 있어 보이는 이슈를 발견하였습니다.
해당 이슈에서는 다음과 같은 내용이 수정되었습니다.

if (index >= 0) {
      panelDataArray.splice(index, 1);
      unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
}

만약 다음과 같이 개별적으로 스타일을 변경하면 계산이 더 자주 발생하게 됩니다.

element.style.width = "100px";
element.style.height = "200px";
element.style.margin = "10px";

이걸 class로 정의해서 한꺼번에 Batch식으로 적용하게 되면 한번의 스타일 변경만 발생합니다.

.new-style {
  width: 100px;
  height: 200px;
  margin: 10px;
}
element.classList.add("new-style");

reflow 최적화할 수 있는 방법 더 찾아보기

  • css transform 사용

단순히 css를 변경하는 것보다 transform 방식을 사용하면 GPU 가속을 통해서 성능을 최적화할 수 있습니다.

/* 기존 방식 (Reflow 발생 가능) */
.box { left: 100px; }  

/* 변경 방식 (GPU 가속 활용, Reflow 없음) */
.box { transform: translateX(100px); }
  • requestAnimationFrame 사용

만약 class를 따로 정의하는 것이 불가하다면 requestAnimationFrame를 사용할 수 있습니다.
해당 기능을 통해 레이아웃 변경을 묶어서 실행할 수 있습니다.

requestAnimationFrame(() => {
  element.style.width = "200px";
  element.style.height = "100px";
});

끝 !

profile
인치

0개의 댓글