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 3 times, most recently 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(...)
 }
}

borisyankov added a commit to borisyankov/react-native-brownfield that referenced this pull request Aug 23, 2026
Review feedback on callstack#450.

`preloadReactNative` becomes the `preload` parameter of
`startReactNative`, which also takes the launch options. The iOS API is
then as small as the Android API, where `initialize` does the same
operation.

`JSBundleLoadObserver` keeps one callback again. A new callback replaces
the callback that waits. Only one method registers a callback now, thus
the list of callbacks is not necessary. The callback continues to run on
the main thread, and it runs immediately if React Native already loaded
the bundle.

The Apple App preloads React Native. The other configuration is before
that call, because the call evaluates the JavaScript bundle.

`BrownfieldReactHostPreloader.m` marks `launchOptions` as nullable, in
the same way as the header.
borisyankov added a commit to borisyankov/react-native-brownfield that referenced this pull request Aug 23, 2026
Review feedback on callstack#450.

`preloadReactNative` becomes the `preloadBundle` parameter of
`startReactNative`, which also takes the launch options. The iOS API is
then as small as the Android API, where `initialize` does the same
operation. The parameter has no default value, thus a preload is always
opt-in, and an app that changes nothing keeps its behavior.

`JSBundleLoadObserver` keeps one callback again. A new callback replaces
the callback that waits. Only one method registers a callback now, thus
the list of callbacks is not necessary. The callback continues to run on
the main thread, and it runs immediately if React Native already loaded
the bundle.

The Apple App preloads React Native. The other configuration is before
that call, because the call evaluates the JavaScript bundle.

`BrownfieldReactHostPreloader.m` marks `launchOptions` as nullable, in
the same way as the header.

Claude-Session: https://claude.ai/code/session_012ny7AHVpsLhDSX6qBpjGZ7
@borisyankov
borisyankov force-pushed the feat/ios-preload-react-native branch from cd72b35 to c567c5e Compare August 23, 2026 21:31
`startReactNative` takes a `preloadBundle` parameter and the launch
options. When it is true, the bundle is loaded and evaluated at launch
and `onBundleLoaded` fires there. This is the iOS counterpart of
Android's `ReactNativeBrownfield.initialize`. The preload is opt-in,
thus an app that changes nothing keeps its behavior.

A plain Objective-C shim makes the call, because Swift cannot:
`RCTReactNativeFactory.devMenuConfiguration` is nullable while the
matching parameter is not. The shim knows the two shapes of
`initializeReactHostWithLaunchOptions:`, and it selects one at run time.

When Brownfield cannot preload, it writes a log and the first React
Native view loads the bundle, as before. This occurs when the bundle URL
can still change, or when the React Native version has no known
`initializeReactHostWithLaunchOptions:` method.
@borisyankov
borisyankov force-pushed the feat/ios-preload-react-native branch from c567c5e to bffeb07 Compare August 27, 2026 11:03
@hurali97
hurali97 requested a lite review from Copilot August 28, 2026 04:45

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

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment on lines +36 to +41
- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
bundleConfiguration:(RCTBundleConfiguration *)bundleConfiguration
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;

- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;
Comment on lines +34 to +36
* Keeps one callback, and calls it one time. A new callback replaces the callback that waits. If
* React Native already loaded the bundle, the class calls the callback in the next turn of the
* main run loop.

@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.

LGTM - Take a look at Copilot's comments and failing CI

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