최근에 새로운 프로젝트로 Electron을 활용해 데스크탑 어플리케이션을 개발하고 있습니다.
이 프로젝트에서의 요구 사항 중 하나는 패널을 분리하고 사용자가 드래그하여 창의 크기를 조절할 수 있는 기능을 구현하는 것이였습니다.
이 gif를 보면 이해가 편합니다.
직접 구현하기 보다는 react-resizable-pannels 라이브러리를 활용하였는데요.
문제는 구현을 완료하고 스트레스 테스트를 하던 중 UI 스레드의 저하 문제가 발견되었습니다.
이 문제가 발생한 이유와 해결책을 찾은 과정, 그리고 어떻게 개선했는지에 대해 회고하겠습니다. 😃
문제 영상부터 보시죠.
위의 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
를 이해하기 위해서는 먼저 브라우저의 렌더링 과정을 살펴봐야 합니다.
여기서 주목할 점은 Layout
입니다.
레이아웃 단계(layout)에서의 reflow(재배치)
는 요소의 크기나 위치 등의 변경으로 인해 다시 레이아웃을 계산하는 과정을 의미합니다.
그리고 이 과정은 매우 많은 비용이 들어가게 됩니다.
즉, 다음들과 같은 원인들 하나 때문에 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");
단순히 css를 변경하는 것보다 transform
방식을 사용하면 GPU 가속을 통해서 성능을 최적화할 수 있습니다.
/* 기존 방식 (Reflow 발생 가능) */
.box { left: 100px; }
/* 변경 방식 (GPU 가속 활용, Reflow 없음) */
.box { transform: translateX(100px); }
만약 class
를 따로 정의하는 것이 불가하다면 requestAnimationFrame
를 사용할 수 있습니다.
해당 기능을 통해 레이아웃 변경을 묶어서 실행할 수 있습니다.
requestAnimationFrame(() => {
element.style.width = "200px";
element.style.height = "100px";
});
끝 !