React Native 常被介紹成「React,但輸出是 native views 而不是 DOM」。這跳過了為什麼 View 裡的字串會 crash、為什麼 scroll offset 上的 useState 會掉 frame、為什麼 Expo config plugin 不能 over the air 送達,或為什麼 useLayoutEffect 可以在一次 commit 裡量到 view,但若你每 frame 都 animate height 仍然會失敗。
這個技術棧透過 Expo 交付 React Native,就像這個網站透過 Next.js 交付 React 一樣。Expo 是圍繞 React Native 的 orchestrator。 React Native 是 React 的 host renderer。Expo 分類 native modules、產生 iOS 與 Android 專案、用 Metro bundle JavaScript,並把 artifacts 對應到 devices。React Native 把 React tree 轉成 C++ shadow tree、跑 Yoga,並 mount UIView / android.view.View instances。React 仍擁有 elements、Fibers、lanes、render 與 commit。
app source + app.json / config plugins
→ Expo prebuild / autolinking / Expo Modules
→ Metro bundle (Hermes bytecode)
→ React render (Fibers)
→ Fabric shadow nodes (C++, via JSI)
→ Yoga layout
→ mount native views
→ native events / C++ state / Expo Modules feed back這篇文章針對 Expo SDK 57(expo@57.0.9、React Native 0.86.2、React 19.2、Hermes V1)。New Architecture 是預設,而且在這條 SDK line 裡,也是唯一受支援的 runtime。四種陳述:
- 一條 React 規則,例如 render purity,或 state setter 會 schedule 工作而不是突變目前的 closure。
- 一份 React Native 契約,例如
<View>與<Text>是不同的 host components。 - 一份 Expo 契約,例如 config plugins、Continuous Native Generation、Expo Modules、
expo-router,或 EAS Build 與 EAS Update 的分工。 - 一項 實作觀察,例如 shadow-node field。應用程式碼不得依賴它們。
Legacy Bridge 與 react-native init 只在舊詞彙會造成歧義時出現。Expo Go 是帶固定 native module set 的 development host。它不是 production architecture。
1. Expo 在 React Native 周圍加上 Policy 與基礎設施
React 定義 components、reconciliation、Suspense 與 concurrent rendering。它不決定 TypeScript screen 如何變成 IPA、哪個 native module 被 link、fonts 如何嵌入,或之後的 JavaScript bundle 如何取代隨 store build 一起出貨的那一份。React Native 決定 host views 如何建立。Expo 決定其餘的 delivery path。
Expo app source
├→ Prebuild, config plugins, autolinking → iOS / Android binary
└→ Metro + Hermes
→ Hermes: React render
→ JSI ─┬→ Fabric shadow tree → Yoga layout → Mount on UI thread → UIView / android.view.View
│ → Native events and C++ state → JSI
└→ TurboModules and Expo Modules → Camera, SecureStore, network
EAS Build / Updates → iOS / Android binary
EAS Build / Updates → Metro + Hermes- Config 與 prebuild 決定 binary 裡存在哪些 native code。
app.json/app.config.ts加上 config plugins 就是契約。npx expo prebuild會 materializeios/與android/。 - Autolinking 在 app package 的
node_modules裡發現 native modules。只存在於 shared workspace package 的 native dependency 是不可見的。 - Metro 把 JavaScript graph 轉成 Hermes 可執行的 bundle。開發時,Fast Refresh 會替換那個 graph。Hermes V1 在 production 把它 compile 成 bytecode。
- React Native 負責 render、layout 與 mount。Expo 不會取代 Fabric。
- EAS 是 deployment adapter。EAS Build 產出 native binary。EAS Update /
expo-updates會替換已包含 matching native surface 的 binary 裡的 JavaScript bundle。
Expo 之於 React Native,就像 Next.js 之於 browser:圍繞 React core 的 policy 與基礎設施。Next.js 把 URLs 對應到 browser 的 React trees。Expo 把 app/ 檔案與 native modules 對應到 device 的 React trees。
2. React Native 是 Renderer,不是 Browser
Browser 解析 HTML、建立 DOM 與 CSSOM、style、layout、paint 與 composite。那條路徑是 Browser 裡的 Critical Rendering Path。React Native 不做任何那些事。沒有 HTML、沒有 CSSOM、也沒有 DOM。JSX 仍產生 React elements。Host tree 是 platform views。
<View> 與 <Text> 不是帶不同 default styles 的 div。它們是 不同的 host components,有不同的 native types 與不同的 layout rules。Yoga 可以 size 一個 View。Text metrics 來自 platform(NSLayoutManager / Android text layout)。字串只能作為 Text 的 child 才合法。
function Badge({ count }: { count: number }) {
return (
<View>
{count > 0 ? <Text>{count}</Text> : null}
</View>
)
}- React Native 會嘗試把 raw 字串 mount 成不接受 text 的 view 的 host child。
<View>Hello</View>在 production 會 crash。 - Web DOM 會把
0stringify 成 text node。Native host tree 不會。 - Expo Go 不會改變這份契約。它是 prebuilt 應用,其 native module set 就是 Expo 在該 Go binary 裡出貨的內容。Config plugin、custom Expo Module 或新的 native dependency 在 你的 binary 裡存在之前,對 Go 不可見。
- Production development 使用 dev client(
expo-dev-client)或 native surface 與app.json匹配的 EAS build。
失敗: {count && <Text>{count}</Text>} 當 count 是 0。JSX render 的是 0,不是 false。Fabric 隨後會嘗試在 View 底下建立 text host node。條件應放在 boolean 或 ternary 裡。
3. 四棵 Tree
深入理解 React 區分 React elements、Fiber tree 與 host tree。Fabric 加上第四棵 tree,它活在 C++ 裡。
React elements descriptions returned by components
Fiber tree React's persistent bookkeeping and unit of work
Shadow tree C++ layout tree (Yoga)
Host view tree UIView / android.view.View on the UI thread- 像
HomeScreen或expo-routerlayout 這樣的 composite component 永遠不會變成 shadow node。Fabric 只為 hosts 建立 shadow nodes:View、Text、ScrollView、來自expo-image的Image。 - Host Fiber 儲存指向其 shadow node 的 JSI pointer。Element 是暫時的描述。Fiber 記住 identity、hooks 與 lanes。Shadow node 持有 style、children 與 layout。Host view 是用戶能看到的。
- 新的 element object 不代表新的
UIView。一次 component render 不代表 mount。JSX 裡的 wrapperView在 flattening 之後可能永遠不會作為 native view 存在。 - Device 上改變的
ScrollViewoffset 不一定經過 React state。
4. JSI 取代了 Bridge
在 New Architecture 之前,JavaScript 與 native code 透過 asynchronous queue 通訊。Calls 被序列化成 JSON、post 過 bridge,並在另一端 decode。Layout 無法在 render 期間被同步讀取。Native modules 被 eager 初始化。Concurrent React 無法 commit 一棵連貫的 native tree。
那條 queue 坐在更緊的耦合上:React Native 是對著 JavaScriptCore 的 C API 寫的。Engine call sites 冗長、要手管 memory,換 engine 意味著 fork framework。JSI(JavaScript Interface)是打破那層耦合的 C++ wrapper。Hermes、JSC、V8 與 Chakra 都可以實作同一份 Runtime。Hermes 存在,是因為有這條縫。
New Architecture 用 JSI 取代 queue,用 Fabric 取代 renderer,用 TurboModules(在 Expo 裡還有 Expo Modules API)取代 native modules。Expo SDK 55 丟掉了 legacy architecture。SDK 57 不把它作為 runtime 選項提供。
JSI 讓 JavaScript engine 持有 native object 的 reference,也讓 native code 持有 JavaScript function 的 reference。JS 對 native 的 call 可以是 synchronous。Hot path 上沒有 JSON round-trip。寫進 C++ shared memory 的 updates,JavaScript 立刻可見。
Runtime 是一等物件,不是口號。幾乎每次 JSI call 都要帶一份。它只能在 JS thread 上讀和寫;離 thread 存取是 undefined behavior。Framework 擁有它的 lifecycle,可以 tear down 再 restart。Capture 它意味著你自己管那份 lifecycle。它不是 singleton。
jsi::Runtime engine-backed JS environment; JS-thread only
jsi::Value C++ representation of a JS value
jsi::Object Value with property get / set
jsi::Function callable Value; how native creates a JS Promise
host function C++ function installed into JS
host object C++ object whose get / set run when JS reads a property- 那是 Fabric 在 render 期間建立 shadow nodes 所用的契約,也是 TurboModules 與 Expo Modules 暴露
SecureStore、camera frames 與 filesystem handles 所用的契約。 - Host function 是裝進 JS 的 C++ function。早期 JSI native modules 走這條路:
jsi::Value→ C++dynamic(一隻 generic bag)→ native types。Host object 是 C++ object,JS 讀 property 時跑它的get/set。TurboModules、Fabric UIManager 與 Expo Modules 都是 host objects。 - Fabric 更快,是因為它丟掉了 deserialization hops,不是因為 JSI 有魔法。Legacy UIManager 是 native module:dynamic → generic prop map → 再 deserialize 一次 → native view。Fabric 的 UIManager 是 host object:把 JSI value wrap 成 RawProps → 一次轉成 native props。
- iOS 多贏這一 hop,因為 Objective-C++ 能更直接地把 JSI values bridge 到 native types。Android 仍要在 Java/Kotlin 與 C++ 之間付 JNI。JSI 不是銀彈。
- 像 VisionCamera 這樣的 library 可以在不把約 30 MB 的 pixels 每秒六十次複製進 JSON string 的情況下處理 frame buffers。
- Synchronous 不代表免費,也不代表「在另一條 thread」。JS thread 上一次重的 JSI call 仍會 stall React。若工作屬於 UI thread,module 必須明確 hop 過去。
- JSI 不是 React API。應用程式碼應繼續呼叫已文件化的 modules。那些 modules 是 JSI host objects 這一事實解釋 latency;它不是你在 screen 裡該去 reach 的 type。
「Bridge」這個詞仍出現在 blog posts 與舊 modules 的 interop layers 裡。它不是 framework 所建立的契約。這些 Runtime 與 host-object 名字是實作觀察。Phillip Pan 的 React Native under the hood(App.js Conf 2024)走一遍它們。
5. Threading
Fabric 保持 renderer structures immutable:updates clone 而不是 mutate。幾乎每條 production bug 裡,有兩條 thread 很重要。
JS thread UI thread
React render host view mutations
most Yoga layout high-priority render (gestures)
TurboModule / Expo Module JS C++ state from native views
Metro / Fast Refresh- JS thread 跑 Hermes、React 的 render phase,以及大部分 Yoga 工作。
- UI thread(main thread)是唯一可以 create、update 或 destroy host views 的 thread。
- 高優先級的 native events 可以在 UI thread 上跑 render pipeline,這樣 gesture 不會卡在 in-flight JS render 後面。較低優先級的工作可以被 interrupt。
- C++ state updates,例如
ScrollViewoffset,可以完全跳過 React 的 render phase。 - CallInvoker 是 native code 從 UI thread 或 background thread hop 上 JS thread 的方式。Dispatch 是 asynchronous。它不讓你 block 當前 thread 等到 JS 跑完。這就是 camera 與 animation libraries 仍需要自己的 UI-thread worklets,而不是從 UI thread 調 JSI 的原因。
- Hermes 不會給你第二條 JS thread。Concurrent React 可以在 JS thread 上 pause 並 restart 一次 render。它不會把 JavaScript 挪到 UI thread 上,除非 renderer 明確在那裡跑一次高優先級 pass。
失敗: 把「app 很慢」當成一種診斷。一次長的 React render 佔用 JS thread。一次 layout-thrashing 的 animation 佔用 Yoga。一個 main-thread native module 佔用 UI thread 並推遲 mount。Reanimated worklets 存在,是因為有些工作根本不能等 JS。
6. Render、Commit 與 Mount
Fabric 的 pipeline 有三個 phases。官方文件稱它們為 render、commit 與 mount。它們對應 React 的 render/commit 切割,再加一步 native publication。
React render (JS)
→ shadow nodes (C++, via JSI)
→ commit: Yoga + promote next tree
→ mount: diff, flatten, mutate UIView / android.view.View- Render。 React 把 composite components 收成 host elements。對每個 host,Fabric 同步在 C++ 裡建立 shadow node。這通常跑在 JS thread 上。React element tree 是暫時的。Fibers 持久存在並持有 JSI pointer。
- Commit。 Fabric 用 Yoga 計算 layout,並把那棵 tree promote 為「next」。大部分 layout 是 C++。
Text與TextInput仍會呼叫 platform 取 metrics。Commit 不得突變 host views。 - Mount。 UI thread 把上一棵已 mount 的 shadow tree 與下一棵 diff,flatten 不需要 host view 的 nodes,並套用原子 mutations:create、update、insert、remove、delete。只有這時 pixels 才會變。
- 初次 render 對空樹做 diff。一次 update 可能只碰一個
backgroundColor。React 可以跳過中間的 trees:一次稍後完成的 render 可以對上一次已發布的 tree mount,而不是對每一個被放棄的嘗試。
這就是 concurrent features 能在 native 上工作的原因。Render 是 speculative 的。Mount 是 publication。一次從未完成的 transition 永遠不會突變 host views。
7. Yoga、Flattening,以及 Layout 可能花多少
Yoga 在 C++ 裡實作一套 Flexbox-like 的 layout。Styles 是 JavaScript objects,不是 CSS stylesheets。gap、padding 與 flex 是 shadow-node inputs。它們不是 browser cascade。
import { useEffect, type ReactNode } from "react"
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated"
function SlideIn({ visible, children }: { visible: boolean; children: ReactNode }) {
const progress = useSharedValue(visible ? 1 : 0)
useEffect(() => {
progress.set(withTiming(visible ? 1 : 0))
}, [progress, visible])
const animatedStyle = useAnimatedStyle(() => {
const value = progress.get()
return {
opacity: value,
transform: [{ translateY: (1 - value) * 24 }],
}
})
return <Animated.View style={animatedStyle}>{children}</Animated.View>
}- View flattening。 只透傳 layout 的 nested wrapper
Views 往往永遠不會變成 host views。Fabric 可以把一棵典型的 600–1000 nodes 的 shadow tree 減到大約 200 個 host views。Native inspector 裡少了一個View,往往是 flattening,不是 missing mount。 - Animate
width、height、top、margin或padding是在請 Yoga 每一 frame 重新計算。Animatetransform與opacity可以在 GPU 上跑,不必 relayout。 - Fabric 讓 synchronous measurement 能在同一一次 commit 裡發生。
useLayoutEffect可以透過getBoundingClientRect(0.82 之後的路徑)在 paint 之前讀 layout。onLayout在 view 稍後改變時保持值是新的。Prefer dispatch updater,這樣相同的 size 不會再 schedule 一次 render。 - 兩種 measurement API 都不是每 frame animate layout properties 的藉口。
失敗: 每 frame animate height,然後奇怪為什麼 Yoga 與 mount 保不住 60fps。
8. Structural Sharing 與 C++ State
Shadow trees 是 immutable 的。一次 React state update 會從改變的 node clone 到 root 的那條 path,並 share 未改變的 subtrees。Mount 把上一棵已發布的 tree 與新的一棵 diff。
Shadow tree 裡的大部分資訊源自 React 並向下流。C++ state 是例外。有些 host components 持有 JavaScript 並不擁有的 state。ScrollView offset 是常見例子:platform view 滾動,C++ 記錄 offset,像 measure 這樣的 APIs 可以讀到它。React 並沒有 render 那次 update。
function Feed() {
const [scrollY, setScrollY] = useState(0)
return (
<ScrollView
onScroll={(event) => {
setScrollY(event.nativeEvent.contentOffset.y)
}}
scrollEventThrottle={16}
/>
)
}- Keys 與 identity 仍然重要:它們決定哪條 Fiber、哪個 shadow node、哪個 host view 能活下來。
- 一個大 screen 若每一列都回傳新的 inline style object,仍可能比截圖看起來做更多工作。即便視覺結果看起來一樣,style 仍是 cloning 的 input。
- 若 React 在中間 commit 了,C++ state commits 會 retry。Source of truth 仍留在 native。
失敗: 把 scroll offset 放進 useState。JS thread 現在每一 frame 都要 reconcile。Offset 已經在 C++ 裡。做 animation 時,Reanimated shared value 在 UI thread 上更新。做非 reactive 的 tracking,一個 ref 就夠。
9. TurboModules、Codegen 與 Expo Modules
Legacy native modules 事先註冊。Startup 為 camera、maps 與 analytics 付錢,哪怕第一屏從未用過它們。
TurboModules 透過 JSI 懶載入。第一次 JS access 才建構 module。Codegen 讀取 TypeScript 或 Flow specs 並產生 native bindings,這樣 JS/native interface 在 build time 被檢查。
Expo 的 Expo Modules API 坐在同一層 JSI 上。它的 Swift 與 Kotlin DSL compile 成 JSI host bindings。expo-secure-store、expo-camera、expo-font 與 expo-image 是帶 JavaScript entry points 的 native modules。它們不是 fetch 外面的可選 wrappers。Screen 仍呼叫已文件化的 module,不是 jsi::Runtime。
C++ TurboModules 收到一份 Runtime,因為它們的 methods 預設跑在 JS thread 上。那是實作觀察,不是從 screen 安裝 raw bindings 的理由。
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": ["./assets/fonts/Geist-Bold.otf"]
}
]
]
}
}- Autolinking 掃描 app 的
node_modules。在 monorepo 裡,packages/ui用到的 native dependency 也必須在 app package 裡宣告。否則 prebuild 不會 link 它。 - Config plugins 突變產生出來的 native project:Info.plist keys、Gradle permissions、embedded fonts、splash screens。Fonts 應放在
expo-fontplugin 裡,這樣它們在 process start 時就存在,而不是等一個 asyncuseFontsgate。 - Images 應放在
expo-image:native caching、blurhash placeholders、recycling keys。react-native的Image是更薄的 host wrapper。 - 加上 plugin 或 native module 會改變 native surface。那需要
npx expo prebuild(或 EAS Build)以及一份新 binary。僅 JavaScript 的 changes 不需要。
10. Metro、Hermes,以及 OTA 能替換什麼
npx expo start 跑 Metro。Metro 從 Expo entry 走 module graph,resolve platform extensions(*.ios.tsx、*.android.tsx),並 serve 一份 bundle。開發時,Fast Refresh 在能保留 state 的地方替換 components。Production 裡,Hermes V1(自 SDK 56 起預設,包括 SDK 57)把 bundle compile 成 bytecode。
- JS thread 仍是一條 thread。更便宜的 bytecode format 不會讓對 feed 做無界
map變免費。 - EAS Update /
expo-updates把新的 JavaScript bundle 出貨到已有的 binary。它不能加上 native module、permission、透過 config plugin 註冊的 font file,或新的 Expo Module。那些活在 IPA/APK 裡。 - Store build 是 native 契約。Update 是給那份契約的一棵新 React tree。
- React Native DevTools 跟 engine 說話。僅 JS 的 freeze 是 Hermes/React 問題。JS 空閒時仍繼續的 freeze 是 UI-thread 或 native-module 問題。
失敗: 一個 screen 在 OTA 之後開始呼叫 expo-camera,對著一份沒有那個 module 的 binary 建置。Update 不會發明 native code。
11. expo-router 是 Native Views 上的 Route Tree
expo-router 把 app/ 下的檔案對應到 screens,就像 Next.js 把 app/ 對應到 URL segments。這個類比有用,但不完整。Next.js 為 document 產生 HTML 與 Flight tree。Expo Router 產生一棵 React tree,Fabric 把它 mount 進 native navigator。
import { Stack } from "expo-router"
export default function Layout() {
return <Stack />
}- 用 native stack。Expo Router 的
Stack是@react-navigation/native-stack。Transitions 與 back-swipe 跑在 UI thread 上。@react-navigation/stack是 JS navigator:它透過 React animate views,並丟掉那種 native behavior。 - 當產品在意 platform feel 時,tabs 也應是 native 的。在 SDK 57 上那是
expo-router/unstable-native-tabs(NativeTabs)。即便那條 module path 稍後穩定下來,也 prefer native tabs。 - Layout file 是 composite component。它有 Fiber identity。它不會自己變成 shadow node。Native stack 擁有 headers、gestures 與 large titles 的 host views。Prefer native header options,而不是自訂 JS header。
- Navigation 是 host tree 的一部分。若 transition 跑在 JavaScript 裡,每一 frame 都會 re-enter React。若它跑在
UINavigationController/ Android native tab host 裡,React 可以在 OS animate 時保持 idle。
12. Native 上的 Concurrent React
Fabric 讓 React 18+ 的 scheduling 在 device 上成為真的。Legacy renderer 無法乾淨地參與 transitions、Suspense,或來自 native events 的 automatic batching。
startTransition可以把一次重的 screen update 標成 interruptible。一個 slider 在設定很大的 list size 時,可以讓 gesture 保持 urgent、list 保持 transitional。- Suspense 可以為 native tree 的一個 region 顯示 fallback,而不拆掉該 boundary 外已 committed 的 host views。
- 在 legacy architecture 上,
onLayout加上後續的setState常常會把 tooltip 畫錯一 frame 位置。有了 Fabric,useLayoutEffect加上getBoundingClientRect可以在一次 commit 裡 measure 並套用。 - Concurrent rendering 仍然不是 parallelism。Hermes 跑這些工作。Fabric 可以 interrupt 它。UI thread 可以接手一次高優先級 pass。已 committed 的 host tree 保持連貫。
至於 React APIs 本身,見 React 19 新功能 與 深入理解 React。那些 APIs 在 Expo SDK 57 上不是 no-ops。
13. 不能等 JS 的工作
60fps 的 gesture 無法 round-trip 過 JS thread 上的 React render、Yoga 與 mount,卻仍感覺 native。Reanimated worklets 與 Gesture Handler 跑在 UI thread 上。SDK 57 pin 了目前版本的 react-native-reanimated、react-native-worklets 與 react-native-gesture-handler。
Pressable 是 host press primitive。TouchableOpacity 是 legacy。Animated press states 屬於 GestureDetector 加上 shared values,而不是 press-in 時的 JS setState。
Lists 是另一個 JS-thread trap。一個把每一列都 map 出來的 ScrollView,會為 offscreen content 建立 Fiber、shadow node,以及可能的 host view。
import { FlashList } from "@shopify/flash-list"
function Feed({ items }: { items: Item[] }) {
return (
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
keyExtractor={(item) => item.id}
/>
)
}- Virtualizer 大約 mount 可見視窗。那是一份 Fabric budget,不是風格偏好。LegendList 是同一想法。契約是「用戶看不見的就不要 mount」。
renderItem上的 inline style objects 與 inline callbacks 在 compiler 沒有 inline 它們時仍會打敗 memoization;hoist,或讓 React Compiler 來做。- Extra render → extra shadow clones → extra mount diffs,發生在 JS 與 UI threads 上,而用戶正在滾動。
14. 從頭到尾跟一次 Press
Expo Router 裡的一張 membership card screen。用戶按下一個 Pressable。Handler 用 setState 寫一個 flag,並從 expo-secure-store 讀一個 token。
- OS 在 UI thread 上把 touch 交給 host view。
- Fabric / Gesture Handler 把 press 路由進 JS,並呼叫
Pressablehandler。 setState在 screen 的 Fiber 上 enqueue 一次 Hook update,並 schedule root(深入理解 React)。- React 在 JS thread 上 render。Composite components 跑。Host elements 被建立。
- Fabric 透過 JSI 為改變的 hosts clone 或建立 shadow nodes,並 share 其餘部分。
- Commit 跑 Yoga。Text metrics 可能呼叫進 UIKit / Android。
- Mount 把上一棵 shadow tree diff,flatten wrappers,並在 UI thread 上突變 native views。
expo-secure-store的讀是另一次進入 Expo Module 的 JSI call。它不經過 Fabric。它可能在 module 內部 hop threads。它永遠不會變成一個View。- 若這次 press 也導航了,native stack 在 UI thread 上 animate,同時 React render 目標 screen。這兩者由 routing 耦合,不是由一次 function call。
React 永遠不會 rewrite 已經跑過的 handler 裡的 pressed binding。一次 half-finished render 永遠不會變成 half-mutated 的 UIView tree。SecureStore 不在 shadow tree 裡。Card 的 View 在。
15. 調試問題與常見誤解
當 Expo screen 意外時,依序想這些:
- 哪條 thread 忙? JS freeze 對 UI freeze 對兩者都 freeze。
- 這是 Expo Go、dev client,還是 release binary? Go 不能長出新的 native modules。
- 是 React render、Fabric commit,還是 mount? Component 裡的
console.log不能證明UIView改變了。 - 這是 C++ state 還是 React state? Scroll offset、text selection 與某些 gesture state 從未進入
useState。 - Flattening 是否移除了 native inspector 裡正在檢查的那個 view?
- 這個 change 需要 native rebuild,還是 JS 就夠? Config plugins、permissions、fonts 與新的 Expo Modules 需要新 binary。EAS Update 不能發明它們。
- Autolinking 是否在查 app package? 只在 workspace library 裡的 native dep 是 missing link,不是 Metro cache issue。
- 這是 host-component 契約嗎?
Text外的字串、0 && <Text />、Gesture Handler list 裡的TouchableOpacity。 - Navigation 是 native 的嗎? JS stack 會在本該免費的 transition 期間顯示為 React 工作。
- Layout 是否每 frame 都在跑? Animated
height是 Yoga。Animatedtransform不是。
這些問題比一直加 memo 直到 list 不再掉 frame 更可靠。
常見誤解:
- React Native 不是 WebView。 WebView 是應用裡的 browser origin。Fabric mount platform views。混用兩者是 security boundary,見 React Native 裡的 Security。
- Expo Go 不是 production 怎麼運作。 Production 是你的 EAS binary,加上可選的、fit 那份 binary 的 OTA JavaScript bundle。
- Managed 不代表沒有 native。 Expo generate native project。應用仍是 UIKit 與 Android views。Config plugins 是 native edits。Expo Modules 是 native code。
- Bridge 不是 SDK 57 上 JS 與 native 怎麼說話。 JSI 是契約。Interop 可能為舊 modules 存在。
- JSI 不代表零 serialization。 早期 JSI modules 仍經過 C++
dynamic轉換。Android 仍要在 Java/Kotlin 與 C++ 之間付 JNI。 - 不要 stash Runtime。 它只屬於 JS thread。Framework 可以 tear down 再 restart。
- JSI 不代表 JavaScript 平行跑。 JSI 是 shared-memory calling。Hermes 仍是一條 JS thread。
- Fabric 不是新的 React。 Fabric 是 React Native 的 renderer。React 的 reconciler 是 深入理解 React 描述的同一 core。
- EAS Update 不能 ship 新的 native module。 它 ship JavaScript。Native surface changes 需要新 build。
- 更多
Views 不代表更多 native views。 Flattening 丟掉 wrappers。Virtualization 拒絕 mount offscreen rows。 useLayoutEffect在 native 上不是沒用。 在 SDK 57 的 Fabric 上,它是 measure 而不 visible jump 的方式。
最終心智模型
最短且準確的模型是:
Expo generates the native binary and the JS bundle.
Hermes runs React.
Fibers remember.
JSI shares memory with native.
Fabric builds a shadow tree.
Yoga lays out.
Mounting publishes UIView / android.view.View.
C++ state can bypass React.
OTA replaces JS, not native.React Native 不會取代 React 的 rendering model。Expo 不會取代 React Native 的 renderer。Expo 決定哪些 modules 存在、native project 如何被 generate,以及 binary 與 bundle 如何到達 device。React Native 決定 React tree 如何變成 platform views。React 決定 updates 如何被 scheduled、rendered 與 committed。
保持 rendering pure,因為 Fabric 可能 restart 它。把字串放在 Text 裡,因為 host tree 不是 DOM。當 UI thread 已經擁有 scroll 與 gestures 時,把它們移出 React state tree。把 config plugins 放在 binary 裡,不要放在 EAS Update 裡。用 native navigators,這樣 transitions 不會 re-enter JavaScript。
至於 React 自己的 queues、lanes 與 commit phase,繼續看 深入理解 React。Web 對照見 Browser 裡的 Critical Rendering Path。Server/document 側對應的 orchestrator 見 深入理解 Next.js。Device threat model(extractable bundle、SecureStore、WebView bridges)見 React Native 裡的 Security。
主要參考:Expo 的 SDK 57 changelog;React Native 的 architecture overview、render pipeline、threading model 與 New Architecture notes;以及 Phillip Pan 的 React Native under the hood(App.js Conf 2024),講 JSI Runtime、host functions 與 host objects。公開的 Expo 與 React Native 契約是耐用的 API。Shadow-node internals 與 Metro 細節是今天 toolchain 的 debugging 證據,不是要 hard-code 的 API。
這篇文章中的實作觀察對應 Expo SDK 57 與 React Native 0.86.2(Hermes V1、New Architecture)。React Native 0.87 是下一個 Expo canary line,不是這個 SDK 的 runtime。