Old Architecture에서는 JS ↔ Native 사이에 Bridge(JSON 직렬화 비동기 큐)가 있어서, 이 브릿지 자체가 병목이 되는 경우가 많았다. New Architecture에서는 JSI(JavaScript Interface)를 통해 JS에서 네이티브 객체를 직접 참조하므로 브릿지가 사라졌다. 하지만 병목 지점이 아예 없어졌다기 보단 바뀌었다고 보는게 맞다.
현재 동작하는 스레드는 크게 세 가지이다.
JS 스레드 — React 컴포넌트 함수 실행, reconciliation(Virtual DOM diffing), 상태 관리, 이벤트 핸들러, 비즈니스 로직. 싱글 스레드이다.
UI(Main) 스레드 — Fabric이 최종 뷰 트리를 받아 실제 네이티브 뷰를 생성 / 업데이트하고 화면에 그린다. 네이티브 애니메이션(Reanimated의 worklet 등)도 여기서 독립 실행된다.
Background 스레드 — Fabric의 레이아웃 계산(Yoga)이 비동기로 실행될 수 있는 스레드. Old Architecture에서 Shadow 스레드라고 불리던 역할인데, Fabric에서는 더 유연하게 스케줄링된다.
핵심 변화는 브릿지 병목이 사라진 대신, JS 스레드의 연산 부하가 성능의 거의 모든 것을 결정하게 됐다는 점이다. JSI 덕분에 네이티브 호출 오버헤드는 거의 없어졌으므로, 이제 순수하게 "JS 스레드에서 얼마나 많은 일을 하느냐"가 체감 성능을 좌우한다.
앱이 버벅이는 가장 큰 원인이다. 여기서 "리렌더링"이란 React 컴포넌트 함수가 재실행되고 Virtual DOM을 다시 만들어 diff하는 과정 전체를 말하며, 이건 100% JS 스레드 작업이다.
API 호출 자체 (네트워크 요청) 는 비동기라 JS 스레드를 블로킹하지 않는다. 문제는 응답이 돌아온 후이다.
API 응답 도착 → setState → React 리렌더 트리거
→ 부모 컴포넌트 리렌더 → 자식 N개 연쇄 리렌더
→ 각각의 reconciliation → Fabric으로 커밋
API가 9개 있고 각 응답마다 상위 컴포넌트가 리렌더되면, React.memo가 없는 자식 7개 섹션이 매번 같이 리렌더된다. 9 × 7 = 63번의 불필요한 컴포넌트 함수 실행이 JS 스레드에서 일어나는 것이다. 실제로는 바뀐 섹션 하나만 리렌더되게 하는 것이 개발자의 의도일 것이다.
이 외에 JS 스레드를 잡아먹는 것들로는 렌더 사이클 안에서의 동기 연산(큰 배열 정렬, JSON 파싱, 날짜 포맷팅 수백 건), 리스트 스크롤 시 renderItem 호출 폭주, 화면 전환 중 새 화면 마운트 + useEffect 실행 등이 있다.
JS 스레드에서 만든 결과물을 Fabric이 받아 실제 네이티브 뷰를 그리는 단계의 문제이다.
뷰 계층이 과도하게 깊거나, opacity + borderRadius + shadow 조합이 많으면 GPU 오버드로잉이 발생한다. 특히 Android에서 체감이 크다. 고해상도 이미지 디코딩이 메인 스레드에서 일어나는 경우, 그리고 애니메이션 중 width/height 같은 레이아웃 속성을 변경해서 매 프레임 Yoga 재계산을 유발하는 경우가 여기 해당된다.
서버 응답이 수 초 이상 걸리는 경우가 아니라면, API 호출 횟수나 타이밍은 체감 버벅임과 거의 무관하다. API를 한 번에 보내든 지연시키든, 응답이 도착하면 동일한 양의 리렌더가 발생하기 때문이다. 다만 응답이 동시에 도착하면 React 18의 automatic batching이 여러 setState를 하나의 리렌더로 묶어주므로 오히려 유리할 수 있다.
[React 리렌더] ──→ [Fabric 커밋] ──→ [네이티브 드로잉]
JS 스레드 JSI 경유 UI 스레드
↑ 이 부분이 ↑ 이 부분이
"React 렌더" "네이티브 렌더"
"UI 렌더링이 원인이다"라고 할 때, 이게 React 컴포넌트의 리렌더(JS 스레드)를 말하는 건지 네이티브 뷰의 드로잉(UI 스레드)을 말하는 건지에 따라 의미가 완전히 달라진다. 실제로 앱이 버벅이는 건 대부분 앞쪽(React 리렌더 = JS 스레드)이 크다.
New Architecture에서 브릿지 병목이 제거된 지금, 체감 성능은 사실상 JS 스레드에서 React가 얼마나 효율적으로 리렌더하느냐에 달려 있다. API 호출 횟수를 줄이는 것보다, 응답 후 바뀐 섹션만 리렌더되도록 격리하는 것(React.memo, Suspense 경계 분리, 상태 colocating)이 체감 개선 효과가 압도적으로 크다.