Archive Lab 04. 카드 그림자를 빼고 스크롤을 다시 측정했다 글 표지
Archive LabReact Native성능 측정Android

Archive Lab 04. 카드 그림자를 빼고 스크롤을 다시 측정했다

카드 그림자를 제거한 앱을 20회 비교해 프레임 지연 P95가 7.4% 줄었습니다. 실제로 줄어든 작업을 확인하고도 사전 채택 기준에 따라 원복한 과정을 기록합니다.

지난 글에서는 FlatList 설정을 두 번 바꾸고 두 번 되돌렸다. 이번에는 카드 그림자를 지웠다. 드디어 측정값이 내려갔고, trace에서도 줄어든 작업을 찾았다.

그런데 이 변경도 되돌렸다. 효과가 없어서 내린 결정은 아니었다. 줄어든 작업을 확인한 뒤, 그 정도의 차이로 화면까지 바꿀지 따로 따져 봤다. 실험 앱의 측정값은 개선됐지만 변경을 되돌렸으므로, 현재 앱에는 그 개선이 적용돼 있지 않다.

한 번에 만드는 양부터 줄여 봤다

그림자에 손대기 전에는 maxToRenderPerBatch를 시험했다. 느린 프레임에서 네이티브 뷰를 만들고 화면에 붙이는 작업이 길게 나타났기 때문이다. 한 번에 처리하는 양을 낮추면 이 작업이 덜 몰릴 거라고 가정했다.

Android에서 기본값 10을 5로 바꿨다. 이 prop은 FlatList가 한 배치에 추가로 렌더링하는 양을 조절한다. 너무 작게 잡으면 빠른 스크롤에서 화면을 제때 채우지 못할 수도 있다. React Native 문서

다만 5라는 값이 한 프레임의 네이티브 뷰 생성 개수를 제한하지는 않는다. 이 앱은 두 열 목록이라 내부 셀 하나에 카드 두 개가 들어간다. 설치된 RN 0.86.3 소스에서도 보이는 영역을 먼저 포함한 뒤 바깥쪽 렌더링 범위를 늘릴 때 이 값을 사용하는 것을 확인했다.

같은 개인 갤럭시에서 기본값 2회, 변경값 2회, 다시 기본값 2회를 측정했다.

배치 실험스크롤 반복frameOverrunMs P95
기본값 102회4.543ms
변경값 52회4.662ms
기본값으로 복귀2회4.635ms

여기서 frameOverrunMs는 프레임이 정해진 마감 시각을 얼마나 넘겼는지 나타낸다. P95는 그 값을 작은 순서로 놓았을 때 95% 지점이다. 낮을수록 좋지만, 전체 프레임 시간이나 FPS와 같은 값은 아니다.

6회에서 6,436프레임을 모았지만 개선 신호는 없었다. 프레임 처리 구간인 doFrame 안에서 새로 만든 이미지 뷰도 변경 전후 모두 최대 두 개였다. 이 관찰만으로 JS 배치 전체를 설명할 수는 없지만, 적어도 기대했던 네이티브 생성량과 mount 비용의 감소는 확인하지 못했다.

바꾼 숫자는 절반이었는데 정작 줄이려던 작업은 비슷했다. 이 설정은 원복했다.

이번에는 카드 그림자 한 줄

다음으로 카드의 elevation을 봤다. Android에서 그림자를 더하고 뷰의 겹침 순서에도 영향을 주는 속성이다. React Native 문서

기존 trace에 그림자 그리기 구간이 있었으니 이 작업을 없앴을 때의 차이를 확인해 보기로 했다. 가장 큰 병목인지는 아직 몰랐다. 비교할 변경이 작고, 제거한 작업이 trace에서도 사라지는지 볼 수 있었다.

- elevation: 1,
+ elevation: 0,

앞서 바꾼 배치 값은 이미 기본값이었다. 카드의 크기, 둥근 모서리, 사진과 텍스트도 그대로 뒀다. 각 후보 APK는 기준 앱과 비교해 JS bundle만 다르고 나머지 1,102개 항목은 같았다.

짧게 두 번씩 비교하니 P95가 4.635ms에서 4.223ms로 내려갔다. 이번에는 반복을 늘려 볼 만했다.

두 번으로 끝내지 않고 20회를 비교했다

본 비교는 기본값을 A, 그림자를 제거한 앱을 B로 두고 A 5회 → B 5회 → B 5회 → A 5회 순서로 진행했다. 마지막에 기본값으로 돌아와서도 차이가 유지되는지 보려는 구성이다. 앞에서 확인한 두 번씩의 값은 이 계산에서 뺐다.

조건은 이전과 같았다. Android 15의 개인 갤럭시, 메타데이터 1,000개, 반복 사용하는 로컬 사진 12장. 개발 서버 없이 실행하는 benchmark 앱을 매회 다시 시작하고 아래로 8번, 위로 8번 스크롤했다. 앱 시작과 데이터 선택은 측정 구간에서 제외했다.

지표기본값 10회그림자 제거 10회
frameOverrunMs P954.852ms4.491ms
frameOverrunMs P999.397ms9.043ms

총 21,418프레임이다. 각 앱의 10회에서 수집한 프레임을 모아 백분위수를 계산했다. 회차별 P95의 평균은 아니다.

P95는 약 7.4%, P99는 약 3.8% 줄었다. 앞쪽 A·B 비교와 뒤쪽 A·B 비교 모두 P95가 낮아졌다. 다만 회차별 값은 꽤 흔들렸다. 기본값만 해도 P95가 약 4.22–6.21ms였으니, 잘 나온 한 회차씩 골라 비교해서는 안 됐다.

측정 도중에는 Mac 저장 공간이 차서 trace 복사가 멈추기도 했다. 기기 측정은 이미 성공한 상태라 원본을 다시 가져와 해시를 대조했다. 측정을 다시 돌려 숫자를 교체한 것은 아니다. 이 복구로 첫 A와 다음 B 사이에 간격이 생긴 점도 기록했다.

배터리 온도는 본 비교 동안 29.7°C에서 31.4°C까지 올랐고, 주사율과 OS 캐시는 강제로 고정하지 않았다. 한 기기에서 얻은 비교 결과다. 프레임이 2만 개 넘는다고 독립적인 실험을 그만큼 한 것도 아니다.

프렌즈의 조이가 아깝다는 표정으로 반응하는 장면
좋아지긴 했는데, 10%는 아니었다.

그림자 비용은 실제로 줄었다

trace에서도 차이를 찾았다. 그림자를 제거한 10개 trace 모두에서 해당 shadow 구간이 사라졌고, RenderThread가 실제로 CPU를 쓰는 시간도 짧아졌다.

회차마다 프레임 처리 구간의 평균 실행 시간을 구해 보면 기본값은 3.972–4.080ms, 그림자를 뺀 쪽은 3.854–3.949ms였다. 마지막에 기본값으로 돌아왔을 때도 비용이 다시 올라왔다. 적어도 이 앱의 스크롤에서는 그림자 제거가 줄여 준 작업을 확인했다.

하지만 네이티브 뷰를 만드는 작업까지 가벼워지지는 않았다. 메인 스레드와 mount 비용에서 같은 개선은 찾지 못했다. 그림자 하나로 느린 프레임 전체를 설명하기는 어려웠다.

10%는 이번 실험에서 정한 조건이었다

7.4%라는 결과를 보고 나니 변경을 남길지 고민할 이유는 있었다. 다만 실험을 시작하기 전에 P95가 10% 이상 줄고, 두 비교 구간에서 같은 방향이며, P99가 5% 넘게 나빠지지 않고 화면·기능에도 문제가 없어야 유지하기로 정해 뒀다.

10%가 사람이 차이를 느끼는 경계이거나 통계적으로 유의한 개선을 뜻하지는 않는다. 이번 실험에서 변경을 남길지 판단하려고 정한 조건이었다. 특히 그림자를 없애면 카드의 모양도 달라진다. 그 선택까지 포함해서 판단했다.

이번 결과는 P95 조건에 못 미쳤다. 결과를 확인한 뒤 기준을 7%로 내리거나 더 크게 줄어든 다른 지표를 앞세우지는 않기로 했다. 그림자는 다시 넣었다.

기능은 두 앱 모두 확인했다. 검색, 즐겨찾기 저장, 상세 화면, 앱 재시작 뒤 복원이 통과했다. 별도로 빠른 왕복 스크롤도 녹화했는데, 변경한 앱에서 처음 나타나는 사진 일부가 잠깐 회색으로 보였다. 같은 시각의 기본 앱 영상은 스크롤 위치가 달라 동일한 조건으로 비교하기 어려웠다. 이 변경 때문에 생겼다고 단정하거나 빈칸이 없다고 결론 내리지는 않았다.

앱 코드는 다시 시작할 때와 같다. 그래도 두 실험에서 얻은 답은 달랐다. 배치 축소에서는 기대한 작업 감소를 확인하지 못했고, 그림자 제거에서는 그리기 비용 감소를 확인했다. 후자는 효과가 있었지만 이번 채택 조건에는 닿지 않았다.

다음에 확인할 것은 남아 있는 네이티브 뷰 생성 비용과 사진이 화면에 채워지는 순간이다. 사진이 보이기 시작한 시점과 로딩 완료 시점을 구분해 재야, 잠깐 보였던 회색 영역도 설명할 수 있을 것 같다.