Skip to content

feat(ios): add preloadReactNative - #450

Open
borisyankov wants to merge 1 commit into
callstack:mainfrom
borisyankov:feat/ios-preload-react-native
Open

feat(ios): add preloadReactNative#450
borisyankov wants to merge 1 commit into
callstack:mainfrom
borisyankov:feat/ios-preload-react-native

Conversation

@borisyankov

@borisyankov borisyankov commented Aug 19, 2026

Copy link
Copy Markdown

Summary

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.

@artus9033 artus9033 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after 2 comments resolved

Comment thread docs/docs/docs/getting-started/ios.mdx Outdated
}
```

`preloadReactNative` makes the app launch slower, but the first React Native screen appears faster. Measure both times in your app.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
`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 }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about logging a message here? CC @hurali97

@artus9033

Copy link
Copy Markdown
Collaborator

Overall that's a great addition to have parity across platforms, thanks for contributing!

@borisyankov
borisyankov force-pushed the feat/ios-preload-react-native branch 2 times, most recently from e59e7f5 to cd4eab8 Compare August 19, 2026 23:39
`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.
@borisyankov
borisyankov force-pushed the feat/ios-preload-react-native branch from cd4eab8 to 94f4065 Compare August 20, 2026 01:16
@hurali97
hurali97 requested a lite review from Copilot August 20, 2026 05:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 onBundleLoaded callbacks 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.

Comment on lines +58 to +60
+ (void)preloadWithReactNativeFactory:(id)reactNativeFactory
launchOptions:(NSDictionary *)launchOptions
{

@hurali97 hurali97 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work with this support 🚀 I have a few suggestions:

  • Let's add the usage of preload in 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(...)
 }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants