Skip to content

React Native Threads and the Bridge — Fundamentals

What it is and why it affects you

A React Native app is two programs cooperating: your JavaScript (components, state, handlers) running on a JS thread, and the platform’s native UI thread doing what every iOS/Android app does — layout, drawing, touch, animation at 60–120fps.

They communicate asynchronously. Historically through the bridge (JSON messages, batched, serialized); in the new architecture through JSI — a C++ layer that lets JS hold references to native objects and call them directly, enabling synchronous calls and shared memory where needed. JSI cuts the overhead dramatically, but the two-thread model is unchanged: your JS still runs on one thread, the screen is drawn by another, and neither waits for the other for free.

Why you care: every classic RN performance mystery is a threading story.

<ScrollView onScroll={(e) => setOffset(e.nativeEvent.contentOffset.y)}>

Scroll events fire on the UI thread → cross to the JS thread → your handler sets state → React re-renders → updates cross back → the view moves. If the JS thread is busy for 100ms (parsing a response, rendering a list), the parallax header you drove from offset freezes for 100ms — while native scrolling continues buttery underneath, because scrolling never needed JS. That contrast — some things stutter while others stay smooth — is the two threads made visible.

The mental model

JS THREAD UI (MAIN) THREAD
───────── ────────────────
your components render measures & draws views
setState, effects, handlers native scroll / gestures
business logic, JSON parsing platform animations
│ │
│ bridge (old): async JSON batches │
└──── JSI (new): direct C++ calls ──────┘
(still two threads)
Busy JS thread → taps queue up, JS-driven animations freeze,
list items appear blank while scrolling fast
Busy UI thread → everything frozen (rare — usually deep native views)
Frame budget → 16.7ms at 60fps; 8.3ms at 120fps

Three rules that explain the cases:

  1. What runs on the UI thread stays smooth no matter what JS is doing. Native scrolling, useNativeDriver animations, Reanimated worklets. This is why the fix for janky animations is “move them to the UI thread”, not “make JS faster”.
  2. What runs per-frame through JS inherits JS’s worst-case latency. A JS-driven animation is only as smooth as the JS thread’s least busy 16ms.
  3. Crossing threads costs; crossing per-frame costs × 60. One onScroll state update per frame = 60 round trips/second competing with everything else JS does.

See the two-thread model live (in browser terms — same physics):

The mobile multiplier: the same JS that hits 16ms budgets comfortably on your dev machine runs on phone CPUs several times slower, with thermal throttling. Mobile is where main-thread discipline (see Runtime fundamentals — the JS thread is exactly that event loop) stops being optional.

Reference table

WorkWhich threadJanks when
Native scrolling, touch recognitionUIalmost never (that’s the point)
Animated with useNativeDriver: trueUI (declared once, driven natively)at setup only; limited to non-layout props (transform, opacity)
Reanimated worklets (useAnimatedStyle, gestures)UI (JS-like code compiled to run there)worklet itself does heavy work
Animated without native driverJS → per-frame updates acrossJS thread busy — the classic jank
onScroll/gesture handlers in plain JSUI event → JS handlerJS thread busy; state-per-frame floods it
Component render, setState, effectsJSbig renders block everything JS-driven
JSON parse of a large responseJSfreezes JS-driven UI while parsing
FlatList item mount/renderJS (then commit to UI)items too heavy → blank cells while scrolling

Frame budgets: 16.7ms (60Hz) / 8.3ms (120Hz displays). A dropped frame on a touchscreen is felt more than on desktop — the finger is on the moving pixel.

How this connects to the cases

  • Animations stutter on mobile — “my animation freezes while data loads” → it’s driven from the JS thread; move it
  • Long lists on mobile — “blank cells and dropped frames while scrolling” → item render cost on the JS thread
  • It must work offline — persistence and sync (the area’s non-threading case — the constraint is connectivity, not frames)

Primary sources