Skip to main content

Toward Hermes being the Default

· 22 min read
Xuan Huang
Xuan Huang
Software Engineer at Meta

2019년 Hermes를 발표한 이후, 커뮤니티에서 점점 더 많은 사람들이 이를 채택하고 있다. React Native 앱을 위한 인기 메타 프레임워크를 관리하는 Expo 팀은 최근 Hermes에 대한 실험적 지원발표했다. 이 기능은 Expo에서 가장 많이 요청된 기능 중 하나였다. 인기 있는 모바일 데이터베이스인 Realm 팀도 최근 Hermes에 대한 알파 지원을 출시했다. 이 글에서는 지난 2년간 Hermes를 React Native를 위한 최고의 JavaScript 엔진으로 만들기 위해 이루어낸 주요 성과를 소개한다. 앞으로 이러한 개선 사항과 더 많은 발전을 통해 Hermes가 모든 플랫폼에서 React Native의 기본 JavaScript 엔진이 될 수 있을 것이라 확신한다.

React Native 최적화

Hermes의 핵심 기능은 사전에 컴파일 작업을 수행한다는 점이다. Hermes를 활성화한 React Native 앱은 일반 자바스크립트 소스 코드 대신 최적화된 바이트코드를 제공한다. 이는 사용자가 앱을 시작하는 데 필요한 작업량을 크게 줄인다. Facebook과 커뮤니티 앱의 측정 결과에 따르면, Hermes를 활성화하면 TTI(Time-To-Interactive) 지표가 거의 절반으로 감소한다.

그러나 React Native에 특화된 자바스크립트 엔진으로서 Hermes를 더욱 개선하기 위해 다양한 측면에서 지속적으로 작업하고 있다.

새로운 가비지 컬렉터를 Fabric에 구현하기

새로운 React Native 아키텍처에서 도입될 Fabric 렌더러를 사용하면 UI 스레드에서 JavaScript를 동기적으로 호출할 수 있다. 그러나 이는 JavaScript 스레드가 실행에 너무 오래 걸리면 UI 프레임 드랍이 발생하거나 사용자 입력이 블로킹될 수 있음을 의미한다. React Fiber가 가능하게 한 동시 렌더링은 렌더링 작업을 작은 단위로 나누어 긴 JavaScript 작업을 스케줄링하지 않도록 한다. 하지만 JavaScript 스레드에서 발생하는 또 다른 일반적인 지연 원인은 JavaScript 엔진이 가비지 컬렉션(GC)을 수행하기 위해 "세계를 멈추는" 경우다.

Hermes의 이전 기본 가비지 컬렉터인 GenGC는 단일 스레드 세대별 가비지 컬렉터였다. 새로운 세대는 일반적인 반공간 복사 전략을 사용하고, 오래된 세대는 마크-컴팩트 전략을 사용해 운영 체제에 메모리를 적극적으로 반환하는 데 탁월했다. 단일 스레드로 동작하는 GenGC는 긴 GC 일시 정지 시간을 유발하는 단점이 있다. Facebook for Android와 같이 복잡한 앱에서 평균 200ms, p99에서는 1.4초의 일시 정지를 관찰했다. Facebook for Android의 크고 다양한 사용자 기반을 고려할 때 최대 7초까지도 걸리는 경우를 확인했다.

이 문제를 완화하기 위해 우리는 Hades라는 새로운 대부분 동시적 GC를 구현했다. Hades는 젊은 세대를 GenGC와 동일한 방식으로 수집하지만, 오래된 세대는 시작 시점 스냅샷 방식의 마크-스위프 컬렉터로 관리한다. 이는 대부분의 작업을 백그라운드 스레드에서 수행해 엔진의 메인 스레드가 JavaScript 코드를 실행하는 것을 블로킹하지 않으므로 GC 일시 정지 시간을 크게 줄일 수 있다. 통계에 따르면 Hades는 64비트 장치에서 p99.9 기준 48ms만 일시 정지한다(GenGC보다 34배 빠르다!) 그리고 32비트 장치에서는 p99.9 기준 약 88ms의 일시 정지 시간을 보인다(여기서는 단일 스레드 점진적 GC로 동작한다). 이러한 일시 정지 시간 개선은 전체 처리량의 손실을 초래할 수 있다. 이는 더 비싼 쓰기 장벽, 느린 프리리스트 기반 할당(범프 포인터 할당자와 달리), 그리고 증가한 힙 단편화 때문이다. 우리는 이러한 절충이 적절하다고 판단했으며, 병합과 추가적인 메모리 최적화를 통해 전체 메모리 소비량을 낮추는 데 성공했다. 이에 대해 더 자세히 설명할 것이다.

성능 문제의 핵심 포인트

앱의 시작 시간은 많은 애플리케이션의 성공에 중요한 요소다. React Native의 성능 한계를 계속해서 넓혀가고 있다. Hermes에 구현하는 모든 새로운 JavaScript 기능에 대해, 프로덕션 성능에 미치는 영향을 꼼꼼히 모니터링하고 지표가 악화되지 않도록 주의한다. 현재 Facebook에서는 Metro에서 Hermes를 위한 전용 Babel 변환 프로필을 실험 중이다. 이를 통해 여러 Babel 변환을 Hermes의 네이티브 ESNext 구현으로 대체했다. 그 결과, TTI(Time to Interactive)가 18-25% 개선되었고, 전체 바이트코드 크기도 줄어들었다. 오픈소스에서도 비슷한 결과를 기대한다.

시작 성능 외에도, React Native 앱의 메모리 사용량을 개선할 여지가 있다는 점을 발견했다. 특히 가상 현실 분야에서 더욱 중요하다. JavaScript 엔진으로서의 저수준 제어를 활용해, 메모리 최적화를 여러 차례 진행했다.

  1. 이전에는 모든 JavaScript 값이 64비트 NaN-boxing 인코딩 태그 값으로 표현되었다. 이는 64비트 아키텍처에서 부동 소수점과 포인터를 나타내기 위함이었다. 그러나 실제로는 대부분의 숫자가 작은 정수(SMI)이며, 클라이언트 측 애플리케이션의 JavaScript 힙은 일반적으로 4GiB를 넘지 않는다. 이를 해결하기 위해 새로운 32비트 인코딩을 도입했다. SMI와 포인터는 29비트로 인코딩되며(포인터가 8바이트 정렬이므로 하위 3비트는 항상 0이라고 가정), 나머지 JavaScript 숫자는 힙에 박싱된다. 이로 인해 JavaScript 힙 크기가 약 30% 감소했다.
  2. 다양한 종류의 JavaScript 객체는 JavaScript 힙에서 GC가 관리하는 다양한 종류의 cell로 표현된다. 이러한 cell의 메모리 레이아웃을 적극적으로 최적화함으로써, 메모리 사용량을 또 다른 15% 줄일 수 있었다.

Hermes의 핵심 결정 중 하나는 JIT(Just-In-Time) 컴파일러를 구현하지 않는 것이었다. 대부분의 React Native 앱에서는 추가적인 준비 비용과 바이너리 및 메모리 공간이 실제로 가치가 없다고 판단했기 때문이다. 수년 동안 인터프리터 성능과 컴파일러 최적화에 많은 노력을 기울여, Hermes의 처리량을 다른 엔진과 경쟁력 있게 만들었다. 인터프리터 디스패치 루프, 스택 레이아웃, 객체 모델, GC 등 모든 곳에서 성능 병목 현상을 식별하고 처리량을 개선하는 데 계속 집중하고 있다. 다음 릴리스에서 더 많은 수치를 확인할 수 있을 것이다!

수직 통합의 선구적 접근

페이스북에서는 대규모 모노레포 내부에 프로젝트를 함께 배치하는 것을 선호한다. 엔진(Hermes)과 호스트(React Native)가 긴밀하게 협력하며 반복 작업을 진행하면서, 수직 통합을 위한 많은 여지를 열었다. 몇 가지 예를 들어보면 다음과 같다:

  • Hermes는 Chrome DevTools Protocol을 통해 기기 내 JavaScript 디버깅을 지원한다. 이는 기존의 "Remote JS Debugging"(데스크톱 Chrome에서 JS를 실행하기 위해 앱 내 프록시를 사용)보다 우수하다. 동기식 네이티브 호출 디버깅을 지원하고 일관된 런타임 환경을 보장하기 때문이다. React DevTools, Metro, Inspector 등과 함께 Hermes 디버거는 이제 Flipper의 일부로, 원스톱 개발자 경험을 제공한다.
  • React Native 앱의 초기화 경로에서 할당된 객체는 종종 오래 지속되며, 세대별 GC(generational GC)가 활용하는 _세대 가설_을 따르지 않는다. 따라서 React Native에서 Hermes를 구성하여 처음 32MiB를 직접 오래된 세대(pre-tenuring)에 할당하도록 했다. 이를 통해 GC 일시 중지를 방지하고 TTI(Time to Interactive)를 지연시키지 않는다.
  • 새로운 React Native 아키텍처는 JSI(JavaScript Interface)에 크게 의존한다. JSI는 C++ 프로그램에 JavaScript 엔진을 내장하기 위한 경량의 범용 API다. JS 엔진을 유지하는 팀이 JSI API 구현도 함께 유지함으로써, 페이스북 규모에서 신뢰할 수 있고 성능이 뛰어나며 검증된 최적의 통합을 제공할 수 있다.
  • JavaScript 동시성 기본 요소(예: promises)와 플랫폼 동시성 기본 요소(예: microtasks)를 의미적으로 정확하고 성능적으로 구현하는 것은 React 동시 렌더링과 React Native 앱의 미래에 매우 중요하다. 역사적으로 React Native의 promises는 비표준화된 setImmediate API를 사용해 폴리필되었다. 현재는 JSI를 통해 네이티브 promises와 microtasks를 사용 가능하게 하고, 웹 표준에 최근 추가된 queueMicrotask를 플랫폼에 도입해 모던 비동기 JavaScript 코드를 더 잘 지원하려고 한다.

전체 커뮤니티와 함께 성장하기

Hermes는 Facebook 내에서 매우 유용하게 활용되고 있다. 하지만 Hermes가 생태계 전반에 걸쳐 경험을 제공할 수 있게 되고, 모든 사람이 이 기능을 활용하며 전체 잠재력을 실현할 수 있을 때까지 우리의 작업은 완료되지 않았다.

새로운 플랫폼으로의 확장

Hermes는 처음에 Android용 React Native에만 오픈소스로 공개되었다. 이후 React Native 생태계가 확장된 다양한 플랫폼에서 Hermes 지원을 확대하려는 커뮤니티 멤버들의 노력을 보며 매우 기쁘게 생각하고 있다.

CallstackReact Native 0.64에서 iOS용 Hermes를 도입하는 작업을 주도했다. 그들은 이를 달성한 과정에 대해 일련의 글을 작성하고 팟캐스트를 진행했다. Mattermost 앱을 기준으로 벤치마크 결과, Hermes는 iOS에서 JSC 대비 시작 시간 약 40% 개선, 메모리 사용량 약 18% 감소를 일관되게 달성했으며, 앱 크기 오버헤드는 단 2.4 MiB에 불과했다. 직접 확인해보는 것을 추천한다: Hermes 성능 확인.

Microsoft는 React Native for Windows 및 macOS에 Hermes를 도입하는 작업을 진행 중이다. Microsoft Build 2020에서 Microsoft는 Hermes의 메모리 영향(워킹 세트)이 React Native for Windows의 Chakra 엔진보다 13% 낮다고 발표했다. 최근 일부 합성 벤치마크에서 Hermes 0.8(Hades와 앞서 언급한 SMI 및 포인터 압축 최적화가 포함됨)이 다른 엔진보다 30%-40% 적은 메모리를 사용한다는 사실을 발견했다. 당연하게도, React Native로 구축된 데스크톱 Messenger의 영상 통화 경험도 Hermes를 기반으로 한다.

마지막으로, Hermes는 Oculus Home을 포함해 Oculus에서 React 기술로 구축된 모든 가상 현실 경험을 지원하고 있다.

커뮤니티 지원

Hermes를 도입하는 데 여전히 장벽이 존재하는 부분을 인지하고 있으며, 이러한 부족한 기능에 대한 지원을 구축하기 위해 노력하고 있다. 우리의 목표는 Hermes가 대부분의 React Native 앱에 적합한 선택이 될 수 있도록 완전한 기능을 갖추는 것이다. 커뮤니티가 이미 Hermes 로드맵에 어떤 영향을 미쳤는지 살펴보자.

  • ProxyReflect는 원래 Facebook에서 사용하지 않기 때문에 Hermes에서 제외되었다. 또한 Proxy를 추가하면 사용하지 않을 때도 속성 조회 성능이 저하될 우려가 있었다. 하지만 MobXImmer와 같은 인기 있는 라이브러리로 인해 Proxy는 빠르게 가장 많이 요청된 기능이 되었다. 우리는 신중하게 평가한 후 커뮤니티를 위해 이를 구현하기로 결정했고, 매우 낮은 비용으로 구현하는 데 성공했다. 이 기능은 우리가 사용하지 않는 기능이기 때문에, 커뮤니티가 안정성을 입증하는 데 의존했다. 먼저 플래그 뒤에서 Proxy를 테스트하고 v0.4v0.5를 위한 opt-in npm 패키지를 생성했으며, v0.7부터는 기본적으로 활성화되었다.
  • ECMAScript Internationalization API Specification (ECMA-402, 또는 Intl)두 번째로 많이 요청된 기능이었다. Intl은 방대한 API 집합이며, 종종 6MB에 달하는 Unicode CLDR 데이터를 포함해야 한다. 이것이 FormatJS (일명 react-intl)와 같은 폴리필과 커뮤니티 JSC의 국제 변형 빌드와 같은 JS 엔진이 매우 큰 이유다. Hermes의 바이너리 크기를 크게 증가시키지 않기 위해, 우리는 운영 체제에 포함된 라이브러리에서 제공하는 ICU 기능을 사용하고 매핑하는 전략으로 구현하기로 결정했다. 단, 플랫폼 간에 약간의 (종종 미미한) 차이가 발생할 수 있다.
    • Microsoft는 Android에서의 지원을 구축하기 위해 협력했다. 이는 ES2020까지의 ECMA-402의 거의 모든 것을 다루며, 크기 영향은 3% (ABI당 57-62K)에 불과하다. 우리는 트위터에서 설문조사를 진행했고, 결과는 Intl을 기본적으로 포함하는 데 강력히 찬성했다. 그래서 우리는 이를 기본으로 포함시켰고, v0.8부터 사용 가능하다.
    • Facebook은 Major League Hacking을 후원하여 원격 오픈소스 펠로우십 프로그램을 시작했다. 지난해, 우리는 Hermes 샘플링 프로파일러를 출시했다. 올해는 펠로우들이 Hermes, React Native, Callstack의 멤버들과 함께 iOS에서 Hermes Intl 지원을 추가할 예정이다. 계속 지켜봐 주길 바란다!
  • 우리는 커뮤니티에 영향을 미치는 문제를 발견하기 위해 함께 노력해 준 사람들에게 감사한다.

요약

요약하자면, 우리의 비전은 Hermes를 모든 React Native 플랫폼에서 기본 JavaScript 엔진으로 준비시키는 것이다. 이미 이 방향으로 작업을 시작했으며, 여러분의 의견을 듣고 싶다.

생태계가 원활하게 Hermes를 도입할 수 있도록 준비하는 것은 매우 중요하다. Hermes를 직접 사용해 보고, 피드백, 질문, 기능 요청, 호환성 문제 등을 GitHub 저장소에 이슈로 등록해 주길 바란다.

감사의 말

Hermes를 개선하기 위해 노력한 Hermes 팀, React Native 팀, 그리고 React Native 커뮤니티의 많은 기여자들에게 감사의 말씀을 전합니다.

또한 이 글을 쓰는 동안 도움을 준 Eli White, Luna Wei, Neil Dhar, Tim Yung, Tzvetan Mikov 그리고 많은 분들께 개인적으로 감사드립니다. (가나다순)