React Native Performance in Marketplace
React Native는 메인 Facebook 앱의 상단 탭을 포함해 Facebook 계열의 여러 앱에서 다양하게 활용된다. 이 글에서 중점적으로 살펴볼 제품은 Marketplace다. Marketplace는 약 12개 국가에서 서비스되며, 사용자가 다른 사용자가 제공하는 상품과 서비스를 발견할 수 있도록 도와준다.
2017년 상반기에 Relay 팀, Marketplace 팀, Mobile JS Platform 팀, 그리고 React Native 팀의 협력을 통해 Year Class 2010-11 기기에서의 Marketplace Time to Interaction(TTI)를 절반으로 줄였다. Facebook은 이 기기들을 저사양 안드로이드 기기로 분류하며, 이 기기들은 모든 플랫폼 또는 기기 타입 중 가장 느린 TTI를 보여준다.
일반적인 React Native 시작 과정은 다음과 같다:
주의: 비율은 대표적이지 않으며, React Native가 어떻게 구성되고 사용되는지에 따라 달라진다.
먼저 React Native 코어(일명 "Bridge")를 초기화한 후, 제품별 JavaScript를 실행한다. 이 JavaScript는 Native Processing Time 동안 React Native가 어떤 네이티브 뷰를 렌더링할지 결정한다.
다른 접근 방식
초기에 우리가 저지른 실수 중 하나는 Systrace와 CTScan이 성능 개선 작업을 주도하도록 내버려 둔 것이다. 이 도구들은 2016년에 많은 쉬운 문제들을 찾는 데 도움을 주었지만, Systrace와 CTScan이 실제 프로덕션 환경을 대표하지 못하며 실제 상황에서 발생하는 문제를 제대로 재현할 수 없다는 사실을 깨달았다. 시간 분할 비율이 종종 틀리거나 심지어 전혀 엉뚱한 경우도 있었다. 극단적인 경우, 우리가 몇 밀리초 정도 걸릴 것으로 예상했던 작업들이 실제로는 수백 또는 수천 밀리초가 걸리기도 했다. 그렇지만 CTScan은 여전히 유용하며, 우리는 이 도구가 프로덕션 환경에 도달하기 전에 약 1/3의 성능 저하 문제를 잡아낸다는 사실을 발견했다.
안드로이드에서 이러한 도구들의 한계는 1) React Native가 멀티스레드 프레임워크라는 점, 2) Marketplace가 뉴스피드와 같은 복잡한 뷰 및 다른 최상위 탭과 함께 배치된다는 점, 3) 계산 시간이 크게 변동한다는 사실에 기인한다. 따라서 이번 반기에는 프로덕션 측정과 분석 결과가 거의 모든 의사결정과 우선순위 설정을 주도하도록 했다.
프로덕션 계측 과정의 여정
프로덕션 계측은 표면적으로는 간단해 보이지만, 실제로는 꽤 복잡한 과정이다. 각각 2-3주에 걸친 여러 번의 반복 주기를 거쳤다. 마스터 브랜치에 커밋을 적용하고, 앱을 Play Store에 푸시하고, 충분한 프로덕션 샘플을 수집해 작업에 대한 확신을 얻기까지의 지연 시간 때문이다. 각 반복 주기에서는 분석이 정확한지, 적절한 수준의 세분화를 가지고 있는지, 전체 시간 범위에 올바르게 합산되는지 확인했다. 알파와 베타 릴리스는 일반 사용자를 대표하지 않기 때문에 의존할 수 없었다. 결국, 우리는 수백만 개의 샘플을 집계해 매우 정확한 프로덕션 트레이스를 매우 세심하게 구축했다.
분석에서 매 밀리초가 상위 메트릭에 올바르게 합산되는지 꼼꼼히 확인한 이유 중 하나는 초기에 계측에 공백이 있음을 깨달았기 때문이다. 초기 분석은 스레드 점프로 인한 지연을 고려하지 않았다. 스레드 점프 자체는 비용이 크지 않지만, 이미 작업 중인 바쁜 스레드로의 점프는 매우 비용이 크다. 결국, 적절한 순간에 Thread.sleep()
호출을 추가해 이러한 차단을 로컬에서 재현했고, 다음과 같은 방법으로 문제를 해결했다:
- AsyncTask에 대한 의존성을 제거했다.
- UI 스레드에서 ReactContext와 NativeModules의 강제 초기화를 취소했다.
- 초기화 시점에 ReactRootView를 측정하는 의존성을 제거했다.
이러한 스레드 차단 문제를 해결함으로써 시작 시간을 25% 이상 단축했다.
프로덕션 메트릭은 우리의 기존 가정 중 일부에 도전하기도 했다. 예를 들어, 시작 경로에서 많은 JavaScript 모듈을 미리 로드했는데, 이는 모듈을 하나의 번들에 함께 배치하면 초기화 비용이 줄어든다는 가정 때문이었다. 그러나 이러한 모듈을 미리 로드하고 함께 배치하는 비용은 이점을 훨씬 초과했다. 인라인 require 블랙리스트를 재구성하고 시작 경로에서 JavaScript 모듈을 제거함으로써, Relay Classic과 같은 불필요한 모듈을 로드하지 않을 수 있었다(Relay Modern만 필요한 경우). 현재, RUN_JS_BUNDLE
분석은 75% 이상 빨라졌다.
제품별 네이티브 모듈을 조사해 얻은 성과도 있었다. 예를 들어, 네이티브 모듈의 의존성을 지연 로딩함으로써 해당 모듈의 비용을 98% 줄였다. Marketplace 시작을 다른 제품과의 경합에서 제거함으로써, 동일한 시간만큼 시작 시간을 단축했다.
가장 좋은 점은 이러한 개선 사항 중 상당수가 React Native로 구축된 모든 화면에 광범위하게 적용될 수 있다는 것이다.
결론
많은 사람들이 React Native의 시작 성능 문제를 자바스크립트의 속도가 느리거나 네트워크 시간이 과도하게 길기 때문이라고 생각한다. 물론 자바스크립트 속도를 높이면 TTI(Time to Interactive)를 상당히 줄일 수 있지만, 이전에 생각했던 것보다 이러한 요소들이 TTI에 미치는 영향은 훨씬 작다.
지금까지의 교훈은 측정, 측정, 또 측정이다! 일부 성능 개선은 런타임 비용을 빌드 시간으로 옮기는 방식에서 얻을 수 있다. 예를 들어 Relay Modern과 Lazy NativeModules가 그렇다. 다른 개선은 코드를 병렬화하거나 죽은 코드를 제거하는 방식으로 작업을 피함으로써 얻을 수 있다. 또한 React Native의 대규모 아키텍처 변경, 예를 들어 스레드 블로킹을 정리하는 것과 같은 방법으로도 성능을 개선할 수 있다. 성능 문제에 대한 만능 해결책은 없으며, 장기적인 성능 개선은 점진적인 도구화와 개선을 통해 이루어진다. 인지 편향이 결정에 영향을 미치지 않도록 주의해야 한다. 대신, 실제 프로덕션 데이터를 신중하게 수집하고 해석하여 미래 작업을 이끌어야 한다.
향후 계획
장기적으로는 Marketplace TTI가 네이티브로 개발된 유사 제품과 비슷한 수준이 되길 바라며, React Native의 성능이 네이티브 수준에 근접하도록 개선할 계획이다. 또한, 이번 분기에 브릿지 시작 비용을 약 80%나 줄였지만, Prepack과 같은 프로젝트를 통해 React Native 브릿지 비용을 거의 0에 가깝게 낮추려고 한다. 더불어 빌드 시간 처리도 더욱 최적화할 예정이다.