feat(ios): add preloadReactNative - #450
Conversation
artus9033
left a comment
There was a problem hiding this comment.
LGTM after 2 comments resolved
| } | ||
| ``` | ||
|
|
||
| `preloadReactNative` makes the app launch slower, but the first React Native screen appears faster. Measure both times in your app. |
There was a problem hiding this comment.
I'd rephrase this - it's not necessarily making the app start slower, but generally it adds overhead in the caller's section. For instance, I'd imagine you can first launch native app (do not preload RN), and then only preload it as soon as you display the first native screen, which would not slow down initial start. I'd state it this way:
| `preloadReactNative` makes the app launch slower, but the first React Native screen appears faster. Measure both times in your app. | |
| `preloadReactNative` adds overhead to the caller's section, but speeds up the first React Native screen appearance. If you preload React Native in your app's launch critical section, then it impacts your app's launch time. |
| jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded) | ||
| } | ||
|
|
||
| guard let reactNativeFactory, canPreloadReactNative() else { return } |
There was a problem hiding this comment.
How about logging a message here? CC @hurali97
|
Overall that's a great addition to have parity across platforms, thanks for contributing! |
e59e7f5 to
cd4eab8
Compare
`startReactNative` only builds the factory - React Native creates and starts the host inside `viewWithModuleName:`, so no JavaScript ran until the first React Native screen was opened. `preloadReactNative` calls `RCTRootViewFactory.initializeReactHostWithLaunchOptions:...` through a plain Objective-C shim, so the bundle is loaded and evaluated at app launch and `onBundleLoaded` fires there. This is the iOS counterpart of Android's `ReactNativeBrownfield.initialize`. The shim is plain Objective-C because Swift cannot make the call: `RCTReactNativeFactory.devMenuConfiguration` is nullable while the matching parameter is not. It declares both `initializeReactHost` shapes and selects at run time, because no header separates RN 0.83 from 0.84. On Expo the preload fails closed until the launch asset is known, so a preload before `AppController` initializes cannot pin the embedded bundle over an update. `bundleURLOverride` is respected, and expo-dev-launcher is vetoed only in Debug. When the preload cannot load the bundle it logs, and the first React Native view loads the bundle instead. `onBundleLoaded` is delivered on the main thread, callbacks are appended instead of replacing each other, a callback fires immediately when the bundle is already loaded, and pending callbacks are cleared in `stopReactNative`. Launch options given to `preloadReactNative` are kept for a host that a view creates later, covering both the guard-skip and off-main race paths.
cd4eab8 to
94f4065
Compare
There was a problem hiding this comment.
Pull request overview
Adds an iOS “preload” pathway so React Native’s host (and thus the JS bundle) can be initialized at app launch rather than waiting until the first RN view is created, aligning iOS behavior with the existing Android initialization approach and updating bundle-load callback semantics.
Changes:
- Introduces
preloadReactNative(...)API on iOS and shared preloading orchestration/state across Expo + Vanilla runtimes. - Refactors bundle-load observation so
onBundleLoadedcallbacks are queued, delivered on the main thread, and can be registered after the bundle already loaded. - Updates documentation and adds a changeset describing the new API and callback behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-brownfield/ios/Vanilla/ReactNativeHostRuntime.swift | Integrates shared preload state; ensures view uses stored launch options; wires bundle-load callbacks through shared observer. |
| packages/react-native-brownfield/ios/ReactNativeBrownfield.swift | Exposes preloadReactNative as public iOS API and clarifies startReactNative callback behavior docs. |
| packages/react-native-brownfield/ios/ReactHostPreloading.swift | Adds shared preload protocol + state (launchOptions storage + preload flow) used by both runtimes. |
| packages/react-native-brownfield/ios/JSBundleLoadObserver.swift | Switches to multi-callback, main-thread delivery, and “fire immediately if already loaded” semantics. |
| packages/react-native-brownfield/ios/Expo/ExpoHostRuntime.swift | Hooks shared preload state into Expo runtime; respects stored launch options; adds Expo-specific preload gating. |
| packages/react-native-brownfield/ios/BrownfieldReactHostPreloader.m | Objective-C shim to call initializeReactHostWithLaunchOptions:... across RN 0.83/0.84 API shapes. |
| packages/react-native-brownfield/ios/BrownfieldReactHostPreloader.h | Public ObjC header for the preloader shim (keeps Swift-side imports RN-free). |
| docs/docs/docs/getting-started/ios.mdx | Updates iOS getting started to use preloadReactNative for launchOptions + early bundle load. |
| docs/docs/docs/getting-started/expo.mdx | Documents Expo-specific behavior where preload may “fail closed” until bundle URL is stable. |
| docs/docs/docs/api-reference/react-native-brownfield/swift.mdx | Adds preloadReactNative API docs and clarifies onBundleLoaded threading/queuing semantics (Swift). |
| docs/docs/docs/api-reference/react-native-brownfield/objective-c.mdx | Adds preloadReactNative API docs and clarifies onBundleLoaded threading/queuing semantics (ObjC). |
| .changeset/lucky-pears-preload.md | Declares minor version bump and summarizes new preload + callback behavior changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| + (void)preloadWithReactNativeFactory:(id)reactNativeFactory | ||
| launchOptions:(NSDictionary *)launchOptions | ||
| { |
hurali97
left a comment
There was a problem hiding this comment.
Great work with this support 🚀 I have a few suggestions:
- Let's add the usage of
preloadin the Apple App - I am wondering to maintain parity with Android, we can encapsulate preload inside startReactNative, just like on Android it is inside
initialize. We can switch the preload behavior by accepting a boolean argument. - The reason for above is to have less user facing APIs to maintain a deterministic usage
- If we move the preload inside startReactNative, we can also relax the callback mechanism and store one instance of it, instead of an array
I am thinking of the following shape:
//usage
ReactNativeBrownfield.shared.startReactNative(launchOptions: nil, preload: true) {
print("loaded RN")
}// ReactNativeHostRuntime
func startReactNative(launchOptions: [:], preload: Bool, callback: () -> {}) {
....
if (preload) {
preloadReactNative(...)
}
}
Summary
startReactNativeonly builds the factory - React Native creates andstarts the host inside
viewWithModuleName:, so no JavaScript ran untilthe first React Native screen was opened.
preloadReactNativecallsRCTRootViewFactory.initializeReactHostWithLaunchOptions:...through aplain Objective-C shim, so the bundle is loaded and evaluated at app
launch and
onBundleLoadedfires there. This is the iOS counterpart ofAndroid's
ReactNativeBrownfield.initialize.The shim is plain Objective-C because Swift cannot make the call:
RCTReactNativeFactory.devMenuConfigurationis nullable while thematching parameter is not. It declares both
initializeReactHostshapesand selects at run time, because no header separates RN 0.83 from 0.84.
On Expo the preload fails closed until the launch asset is known, so a
preload before
AppControllerinitializes cannot pin the embedded bundleover an update.
bundleURLOverrideis respected, and expo-dev-launcheris vetoed only in Debug. When the preload cannot load the bundle it logs,
and the first React Native view loads the bundle instead.
onBundleLoadedis delivered on the main thread, callbacks are appendedinstead of replacing each other, a callback fires immediately when the
bundle is already loaded, and pending callbacks are cleared in
stopReactNative.Launch options given to
preloadReactNativeare kept for a host that aview creates later, covering both the guard-skip and off-main race paths.