Archive Lab 02. 목록을 5,000개로 늘리면 얼마나 느려질까? 글 표지
Archive LabReact Native성능 측정Android

Archive Lab 02. 목록을 5,000개로 늘리면 얼마나 느려질까?

목록 규모를 100개에서 5,000개로 늘려 Android 스크롤을 30회 측정했습니다. 비슷하게 나온 결과와 frameOverrunMs를 FPS처럼 읽으면 안 되는 이유를 정리합니다.

1편에서는 이미지와 링크를 모아 보는 RN 앱을 만들었다. 길게 늘어나던 사진도 고쳤으니, 이제 스크롤 성능을 확인할 차례였다.

가장 먼저 바꿔 볼 만한 건 데이터 개수였다. 100개일 때는 괜찮아 보여도 1,000개, 5,000개를 넣으면 차이가 생기지 않을까? 같은 화면에 데이터만 늘려서, 어디서부터 수치가 달라지는지 보기로 했다.

결과부터 말하면 생각보다 비슷했다. 100개와 5,000개의 스크롤 측정값은 거의 같은 범위에 있었다. 그래서 이번 기록은 “5,000개도 잘 돌아간다”보다는, 내가 측정한 숫자가 어디까지 말해 주는지 확인한 과정에 가깝다.

손으로 스크롤하는 대신 같은 동작을 반복했다

측정은 개인 갤럭시의 Android 15에서 진행했다. 앱은 Expo 57과 RN 0.86.3을 사용했고, 디버깅을 끈 benchmark 빌드에 JS와 사진을 함께 넣었다. Metro 연결 없이 실행되는 상태다.

손으로 내려 보고 “이 정도면 부드럽네”라고 판단하면 다음 변경과 비교하기가 어렵다. 스크롤 속도도, 내려가는 거리도 달라지기 때문이다. Android Macrobenchmark로 같은 동작을 반복하게 했다.

매번 앱 프로세스를 다시 시작하고, 정해진 데이터 개수를 선택했다. 첫 카드가 준비된 것을 확인한 다음 화면에 보이는 목록 영역 안에서 스와이프했다. 손가락이 움직이는 시작·끝 좌표를 그 영역 높이의 20%와 80% 지점으로 고정하고, 아래로 8번·위로 8번 움직였다. 스와이프마다 같은 60-step 설정을 사용했다.

실제 측정 부분은 이 정도다.

repeat(8) { check(device.swipe(x, bottom, x, top, 60)) }
repeat(8) { check(device.swipe(x, top, x, bottom, 60)) }
device.waitForIdle()

여기서 60은 FPS 목표가 아니라 스와이프를 수행하는 단계 수다. 앱 시작과 데이터 선택은 이 코드가 실행되기 전에 끝낸다. 검색하거나 상세 화면을 여는 시간도 이번 수치에 들어가지 않는다. 준비된 목록을 같은 구간에서 스크롤할 때의 프레임만 비교했다.

사진 조건도 고정했다. 720×540 JPEG 12장을 반복해서 사용하고, 제목·태그 같은 메타데이터 항목 수만 100개, 1,000개, 5,000개로 늘렸다. 서로 다른 사진 5,000장을 불러오는 실험은 아니다.

프로세스를 재시작해도 캐시가 전부 비워지는 것은 아니다. 첫 화면을 준비하는 동안 일부 사진은 이미 로드되고, OS 파일 캐시는 통제하지 않았다. 처음 보는 사진이 계속 들어오는 피드와는 조건이 다르다.

데이터는 50배, 수치는 비슷했다

각 규모를 10회씩, 총 30회 측정했다. 15분 남짓 동안 프레임 32,109개와 회차별 trace 30개가 모였다.

메타데이터 항목 수반복프레임 수overrun P95overrun P99
10010회10,6704.90ms8.23ms
1,00010회10,7174.80ms8.33ms
5,00010회10,7224.83ms8.33ms
수식이 겹쳐 보이는 얼굴로 계산을 다시 해 보는 장면
분명 데이터는 50배로 늘렸는데요.

P95는 세 조건 모두 4.8~4.9ms였다. 항목 수에 따라 꾸준히 올라가는 모양은 보이지 않았다. 오히려 100개 쪽이 조금 높지만, 이 작은 차이로 1,000개가 더 빠르다고 말할 수는 없다.

FlatList는 전체 데이터 개수만큼 카드를 한꺼번에 화면에 만드는 구조가 아니다. 기반인 VirtualizedList는 제한된 범위의 항목을 렌더링하고, 스크롤에 따라 그 범위를 바꾼다. 데이터 개수와 지금 그려야 하는 카드 수가 같지 않다는 점은 이번 결과를 생각할 때 참고할 만하다. React Native 문서

다만 가상화를 끈 대조 실험을 한 것은 아니다. 이 결과만으로 “가상화 덕분에 차이가 없었다”라고 원인을 확정하지는 않았다. 같은 앞쪽 구간을 왕복했고, 사진도 12장을 재사용했다. 전체 목록을 끝까지 훑은 결과도 아니다.

측정 순서 역시 1,000 → 5,000 → 100개로 고정돼 있었다. USB에 연결한 개인 기기였고, 시작과 끝의 배터리 온도는 28.1°C와 30.4°C였다. 주사율을 고정하거나 다른 백그라운드 작업을 모두 통제하지도 않았다. 이런 조건에서 소수점 아래 차이를 데이터 개수만의 효과로 읽기는 어렵다.

4.90ms면 충분히 빠른 걸까?

표에서 가장 헷갈리기 쉬운 부분이 단위다. overrun P95 = 4.90ms는 프레임 하나를 4.90ms 만에 만들었다는 뜻이 아니다.

frameOverrunMs프레임의 정해진 기한을 얼마나 넘겼는지 나타낸다. 양수면 기한을 넘겼고, 음수면 그만큼 여유 있게 끝났다는 의미다. Android FrameTimingMetric 문서

P95는 그 값들을 작은 순서로 놓았을 때 95% 지점이다. 100개 조건에서는 수집한 프레임의 약 95%가 4.90ms 이하의 overrun 값을 가졌다는 뜻이다. 회차별 P95 열 개를 평균 낸 값이 아니라, 해당 조건의 전체 프레임 표본으로 계산한 값이다.

그러니 4.90ms를 16.67ms와 바로 비교해서 “60fps 예산 안에 들어왔다”고 할 수도 없고, 역수를 구해 FPS로 바꿀 수도 없다. 세 조건의 값이 비슷하다는 것과 기한을 넘기는 프레임이 없다는 것도 다른 이야기다. 이 표에는 여전히 기한을 넘긴 구간이 남아 있다.

RN의 JS FPS를 잰 것도 아니다. 이번 지표는 Android 화면 프레임을 대상으로 한다. 함께 수집한 CPU 프레임 구간도 UI 메인 스레드와 RenderThread에 걸친 시간이며, JS가 실행된 시간만 떼어 낸 값은 아니다. 검색어를 입력한 뒤 결과가 바뀌기까지 얼마나 걸리는지는 따로 측정해야 한다.

60Hz라고 적혀 있었는데 기한은 8.33ms였다

한 가지 더 확인할 일이 있었다. 측정 전 대기 상태에서 확인한 화면 모드는 60Hz였다. 그렇다면 프레임마다 약 16.67ms를 기준으로 보면 될 것 같았다.

그런데 본 측정과 별도로 남긴 1회 시험 실행의 trace를 열어 보니, 앱의 expected frame 1,071개에 기록된 기한은 모두 약 8.33ms였다. 반면 expected frame이 시작되는 간격은 대부분 약 16.6ms였다.

화면 주사율, 앱에 주어진 프레임 기한, 앱 프레임의 시작 간격을 같은 숫자로 취급하면 설명이 맞지 않았다. 8.33ms 기한이 기록됐다고 앱이 계속 120fps로 표시됐다고 말할 수도 없다. 이 확인은 지표를 해석하기 위한 별도 시험 실행이어서, 위 30회 결과에 합치지 않았다.

원본을 더 보니 양의 overrun이 있는 프레임 수와 시스템이 늦게 표시됐다고 분류한 프레임 수도 달랐다. 그래서 overrun의 양수 비율을 그대로 “드롭률”이라고 부르지 않기로 했다.

다음에는 개수보다 프레임 안을 보기로 했다

이번 조건에서는 데이터 개수를 늘릴수록 스크롤 수치가 크게 악화되는 패턴이 나오지 않았다. 여기서 10,000개, 50,000개로 더 늘리기보다는, 이미 기한을 넘긴 프레임 안에서 어떤 작업이 일어났는지 보는 쪽으로 방향을 잡았다.

3편에서는 그 trace를 보고 FlatList 설정을 바꿔 본 과정을 쓴다. 화면 밖의 뷰를 다루는 비용이 눈에 들어왔고, 그 비용을 줄이면 전체도 빨라질 거라 생각했다. 실제 비교에서는 예상하지 못한 다른 비용이 늘었다. 그래서 적용했던 설정을 다시 지웠다.