One visual guide in two parts: Beginner (Bridge in the center) → Advanced (Classic Bridge vs JSI / Fabric). Follow JSX to native UI, then compare both architectures with the same
Counter.
Topics: React Native · Bridge · JSI · Fabric · TurboModules · Interview Prep
| Part | Style | Interactive demo |
|---|---|---|
| Part 1 | Beginner — one story, Bridge in the center | react-native-how-it-works-animation.html |
| Part 2 | Advanced — Classic Bridge vs New Arch | react-native-behind-the-scenes-animation.html |
- What you’ll learn
- Part 1 — How React Native works (Bridge story)
- Part 2 — Classic Bridge vs New Architecture
- Interview cheat sheet
- Quick challenges
- Conclusion
Most people think React Native is “React in a WebView.”
It is not.
You write React/JSX. React Native turns that into real native views — UIView on iOS, android.view on Android.
Part 1 path: JS Thread → Bridge → Shadow Thread (Yoga) → UI Thread → Screen
Part 2 upgrade: Bridge JSON → JSI / Fabric / TurboModules
Part 1 (Beginner)
- Why React Native is not a WebView
- What the JS Thread, Bridge, Shadow Thread, and UI Thread each do
- How first render and a tap travel through the Bridge
Part 2 (Advanced)
- Why the Bridge can become a bottleneck
- What JSI, Fabric, and TurboModules each replace
- How the same
setStateround-trip looks in Classic vs New Arch
Demo: open react-native-how-it-works-animation.html and press Play.
| Panel | Role |
|---|---|
| Your RN Code | JSX you wrote (View, Text, Button) |
| JS Thread | Runs React + your business logic (Hermes / JSC) |
| Bridge | Messenger — async JSON between JS and Native |
| Shadow Thread | Builds layout tree; Yoga computes x, y, width, height |
| UI Thread | Creates and paints real native views |
| Phone Screen | What the user actually sees |
- 🔵 JS Thread → React + your code
- 🟡 Bridge → messenger (JSON)
- 🟢 Shadow Thread → layout (Yoga)
- 🔴 UI Thread → real native views
1) JS builds a React tree
2) Bridge carries a message to Native
3) Shadow/Yoga calculates layout
4) UI Thread paints native views
5) Events travel the other way through the Bridge
function App() {
return (
<View>
<Text>Hello</Text>
<Button title="Tap" onPress={onTap} />
</View>
);
}Goal:
- Turn this into a real native screen
- Handle a tap and run
onPressback in JS
flowchart TD
JS[JS Thread<br/>React + your code]
BR[Bridge<br/>async JSON messenger]
SH[Shadow Thread<br/>Yoga layout]
UI[UI Thread<br/>native views]
SC[Phone Screen]
JS -->|create View / Text / Button| BR
BR --> SH
SH --> UI
UI --> SC
SC -->|tap| UI
UI -->|Button pressed| BR
BR -->|onPress| JS
ASCII version
React Native (Classic story)
┌──────────────┐ ┌──────────────┐
│ JS Thread │◄───────►│ Bridge │
│ React + logic│ JSON │ messenger │
└──────────────┘ └──────┬───────┘
│
┌──────────────┼──────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Shadow Thread │ │ UI Thread │
│ Yoga layout │───────────►│ native views │
└────────────────┘ └───────┬────────┘
▼
┌────────────┐
│ Screen │
└────────────┘
Same steps as the beginner animation.
React Native = write React code, show real native UI (not HTML in a browser).
Three workers + one messenger: JS Thread, Shadow Thread, UI Thread, and the Bridge in the center.
JS Thread runs your React code.
App()starts.
Hermes/JSC executes JavaScript. No native buttons exist yet.
Active: Run App()
React creates an element tree from JSX:
View,Text,Button.
This tree is still JavaScript objects — not real iOS/Android views.
JS cannot create native views by itself. It sends a message through the Bridge.
Classic RN: Bridge messages are async JSON.
Message: create View, Text, Button
Shadow Thread builds a Shadow Tree from that message.
Shadow Tree = layout structure (still not pixels).
Yoga calculates layout: x, y, width, height for every node.
Flexbox styles from JS become real positions here.
UI / Main Thread creates real native views (
UIView/android.view).
This is why RN apps feel native — they use platform UI components.
Native views are drawn. User finally sees “Hello” and the Tap button.
JS → Bridge → Shadow/Yoga → UI → Screen
User taps the button. Touch is handled first on the UI Thread (native).
Native side felt the press. JS does not know yet.
Native sends “Button pressed” back through the Bridge to the JS Thread.
Native → Bridge → JS
Then your onPress / setState can run.
JS thinks, Bridge talks, Shadow lays out, UI paints.
New Architecture replaces Bridge JSON with JSI / Fabric — same idea, faster path. That’s Part 2.
flowchart TD
A[JSX: View / Text / Button] --> B[JS Thread builds React tree]
B --> C[Bridge message: create views]
C --> D[Shadow Tree + Yoga layout]
D --> E[UI Thread creates native views]
E --> F[Screen shows Hello + Tap]
F --> G[User taps]
G --> H[UI Thread detects press]
H --> I[Bridge message: Button pressed]
I --> J[JS runs onPress / setState]
ASCII version
JSX → JS Thread → Bridge → Shadow/Yoga → UI Thread → Screen
↑
User tap → UI Thread → Bridge → JS onPress ─┘
Think of React Native as a restaurant with a runner:
- 👨🍳 JS Thread = Chef deciding the menu (React tree)
- 📨 Bridge = Runner carrying tickets (JSON messages)
- 📐 Shadow / Yoga = Floor plan — where every plate goes
- 🍽️ UI Thread = Servers placing real plates on the table
- 📱 Screen = What the guest sees
Tap: guest presses the bell (UI) → runner brings the ticket (Bridge) → chef reacts (JS onPress).
Demo: open react-native-behind-the-scenes-animation.html, switch Classic / New Architecture, then press Play.
The React mental model stays the same.
What changes is how JS talks to native.
| Architecture | Communication layer |
|---|---|
| Classic | Bridge — async JSON messages |
| New Arch | JSI + Fabric + TurboModules — more direct calls |
| Panel | Meaning |
|---|---|
| React Native Code | Your JSX / hooks |
| JS Thread | Hermes + React + business logic |
| Bridge or JSI / Fabric | How JS ↔ Native communicate |
| Shadow Tree / Yoga | Layout calculation |
| UI / Main Thread | Real native views + touch |
| Device Screen | What the user sees |
Classic
JS → Bridge (JSON) → Shadow/Yoga → UI Thread → Screen
(and back through Bridge for events)
New Architecture
JS → JSI / Fabric → Shadow Tree → UI Thread → Screen
(TurboModules for native APIs)
function Counter() {
const [n, setN] = useState(0);
return (
<View>
<Text>{n}</Text>
<Button title="+" onPress={() => setN(n + 1)} />
</View>
);
}We follow twice (Classic, then New Arch):
- Boot + first render (
n = 0) - User taps
+ setN(n + 1)+ re-render- Native Text updates to
1
flowchart LR
subgraph classic [Classic Architecture]
CJS[JS Thread] -->|async JSON| CBR[Bridge]
CBR --> CSH[Shadow / Yoga]
CSH --> CUI[UI Thread]
CUI --> CSC[Screen]
end
subgraph modern [New Architecture]
NJS[JS Thread] -->|direct calls| NJSI[JSI / Fabric]
NJSI --> NSH[Shadow Tree / Yoga]
NSH --> NUI[UI Thread]
NUI --> NSC[Screen]
end
ASCII version
Classic: JS → Bridge (JSON) → Shadow/Yoga → UI → Screen
New Arch: JS → JSI / Fabric → Shadow Tree → UI → Screen
Hermes loads the JS bundle. RN runtime is ready on the JS Thread. Native UI is still empty.
Counter() + useState(0) + JSX element tree. Still only JavaScript — no real UIView yet.
Async JSON over the Bridge:
serialize → queue → deserialize
Message: JSON: create View / Text / Button
Shadow Tree + Yoga layout (x, y, width, height). Not pixels yet.
Real native views mounted. Screen shows 0 and +.
Touch handled by native Button on the UI Thread first.
Native → Bridge → JS Thread
setN(n + 1) · React re-renders · reconcile Text: 0 → 1.
Another JSON message: update Text props. Serialize cost again.
Shadow Tree updates → UI Thread paints 1.
Classic complete:
JS → Bridge → Shadow/Yoga → UI Thread → Screen
Takeaway: Bridge is powerful, but async + JSON can become a bottleneck.
Same React code. Different communication layer.
Hermes + New Architecture. JSI available. TurboModules can load lazily.
Same Counter() / useState / JSX. Same React mental model — different native path.
No JSON Bridge bottleneck. JSI lets JS call C++/native directly. Fabric receives UI commands.
Immutable Shadow Tree + Yoga, with better concurrent rendering support.
Native views from Fabric’s shadow tree. Screen shows 0 and +.
Touch still starts on the native UI Thread.
Event reaches JS through JSI — more direct than Bridge JSON.
setN(n + 1) · re-render · reconcile Text 0 → 1. Your React code is unchanged.
Fabric updates the Text Shadow Tree node via JSI-backed pipeline.
Trio: JSI + Fabric + TurboModules
Yoga commit + UI Thread paints 1.
New Arch complete:
JS → JSI / Fabric → Shadow Tree → UI Thread → Screen
Interview line: Bridge = async JSON. JSI = direct host object calls.
| Step | Classic (Bridge) | New Architecture |
|---|---|---|
| Boot | Hermes + Bridge runtime | Hermes + JSI / Fabric / TurboModules |
| First UI command | Async JSON over Bridge | JSI / Fabric UI commands |
| Layout | Shadow Thread + Yoga | Fabric Shadow Tree + Yoga |
| Native mount | UI Thread | UI Thread (same idea) |
| Tap → JS | Bridge JSON event | JSI direct event |
setState update |
Another Bridge JSON message | Fabric Shadow Tree update via JSI |
| Native APIs | Legacy Native Modules | TurboModules (lazy, typed) |
flowchart TD
subgraph same [Same for both]
R[React Counter + useState]
S[Shadow / Yoga layout]
U[UI Thread native views]
P[Device Screen]
end
R --> C{Architecture?}
C -->|Classic| B[Bridge async JSON]
C -->|New Arch| J[JSI + Fabric]
B --> S
J --> S
S --> U
U --> P
No. It renders real native views.
| Piece | Role |
|---|---|
| JS Thread | React + logic |
| Bridge | Classic messenger — async JSON |
| JSI | Direct JS ↔ native host object access |
| Fabric | New rendering system / Shadow Tree |
| TurboModules | Lazy, typed native modules |
| Shadow Thread + Yoga | Layout calculation |
| UI Thread | Native views + drawing + touch |
Classic: JS and Native talk through an async JSON Bridge; Yoga lays out; UI Thread mounts real views.
New Arch: JSI allows direct calls; Fabric is the new renderer; TurboModules are lazy native modules — less JSON serialization.
React Native runs React on the JS Thread and paints real native views on the UI Thread. Classic RN connects them with an async JSON Bridge; the New Architecture replaces that path with JSI, Fabric, and TurboModules — while your component code stays the same.
function App() {
return (
<View>
<Text>Hello</Text>
<Button title="Tap" onPress={onTap} />
</View>
);
}Reveal answer
First paint
JS Thread → Bridge → Shadow/Yoga → UI Thread → Screen
Button press
UI Thread (native tap) → Bridge → JS Thread (onPress)
A) JS → Bridge (JSON) → Shadow/Yoga → UI → Screen
B) JS → JSI/Fabric → Shadow Tree → UI → Screen
- Which is Classic / New?
- Where is the touch detected first?
- How does the Text update reach native in each?
Reveal answer
- A = Classic, B = New Architecture
- Touch is detected first on the UI Thread in both
- Classic: event + update via Bridge JSON
New Arch: event via JSI, update via Fabric Shadow Tree, then UI paints1
Part 1: Once you watch one screen travel through the Bridge, RN stops feeling magical.
JS thinks → Bridge talks → Shadow lays out → UI paints
Part 2: Same React. Two native highways.
- Classic = Bridge + async JSON
- New Arch = JSI + Fabric + TurboModules
- Yoga + UI Thread still produce real native UI
Try the animations: