Skip to content

Conversation

@tjzel
Copy link
Contributor

@tjzel tjzel commented Sep 24, 2025

Summary:

This PR aims to expose the JS Bundle present in the React Native app to be consumed to Native Modules. This is key feature for libraries doing multi-threading in JavaScript, as they can benefit from well optimized bundle.

ios

I implemented a new "BundleProvider" interface that is set on Native Modules that specify @synthesize bundleProvider = _bundleProvider with the decorator, analogous to other decorated properties.

BundleProvider interface holds the Bundle and the SourceURL of the bundle. Both of these are needed to evaluate the code on a Hermes Runtime.

Android

Since Android generally has public API for Native Modules in ReactApplicationContext, I added getBundle method that returns the wrapper for the Bundle. There's already getSourceURL method.

Common

To facilitate these changes I had to change the way the Bundle is passed to ReactInstance.cpp, but only slightly. Instead of passing std::unique_ptr<const JSBigString> as the bundle I'm passing const std::shared_ptr<const BigStringBuffer>. However, this change isn't significant as ReactInstance.cpp made the buffer from the JSBigString immediately after receiving it.

Possible improvements

It would be even better to distribute the Bundle as a result of prepareJavaScript JSI function. However, this API has beed marked as experimental for 6 years and I don't know if you consider it stable.

Changelog:

[GENERAL] [ADDED] - Expose JS Bundle for Native Modules

Test Plan:

I'm dogfooding these changes in Reanimated repo: software-mansion/react-native-reanimated#8293

iOS

Add @synthesize bundleProvider = _bundleProvider to an iOS Native Module and see that the bundle is defined there.

Android

Obtain the bundle with context.getBundle() in Java Native Module and forward it to C++ like in JReactInstance.cpp to see that it's defined.

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 24, 2025
@tjzel tjzel marked this pull request as ready for review September 25, 2025 11:53
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 25, 2025
Comment on lines -135 to -148
void JReactInstance::loadJSBundleFromAssets(
jni::alias_ref<JAssetManager::javaobject> assetManager,
const std::string& assetURL) {
const int kAssetsLength = 9; // strlen("assets://");
auto sourceURL = assetURL.substr(kAssetsLength);

auto manager = extractAssetManager(assetManager);
auto script = loadScriptFromAssets(manager, sourceURL);
instance_->loadScript(std::move(script), sourceURL);
}

void JReactInstance::loadJSBundleFromFile(
const std::string& fileName,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to "BundleWrapper.cpp".

github-merge-queue bot pushed a commit to software-mansion/react-native-reanimated that referenced this pull request Sep 30, 2025
## Summary

Based on changes from
facebook/react-native#53928

## Test plan

Bundle Mode works for Android and iOS.
@huntie
Copy link
Member

huntie commented Oct 24, 2025

This is key feature for libraries doing multi-threading in JavaScript, as they can benefit from well optimized bundle

@tjzel Can you concretely expand on this and how you're aiming to improve Reanimated with this API?

Sorry, I'm coming in without much context on the change 😅

@tjzel
Copy link
Contributor Author

tjzel commented Oct 24, 2025

@huntie Sure! This is an (updated) proposal on the feature I made a while back:


Motivation

JavaScript multi-threading tools in React Native available now - namely react-native-worklets, react-native-reanimated and react-native-worklets-core work on the principle of creating new Hermes instances and passing functions’ code as strings to be evaluated on them at runtime. This is very limiting and has serious performance flaws. I want to add APIs in React Native that would allow library authors to have access to the JavaScript Bundle in the native code to overcome these limitations. The addition of these APIs should not introduce any breaking changes to React Native.

Why

Multiple libraries like react-native-reanimated, react-native-gesture-handler, react-native-vision-camera, react-native-live-markdown and many more had shown that utilizing separate Hermes runtime instances, executed on threads different than the JS thread, are the key for making React Native apps feel more native by reducing delays and distributing workload. I want to make these possibilities more feature-rich.

Current approach

Libraries aim to offload the execution of certain functions to additional Hermes instances. These Hermes instances are bare - they don’t know anything about the JavaScript bundle and have no access to it.

A (Hermes) JavaScript function instance cannot be copied between runtimes. The way to execute a given function from a given runtime on another is done via strings. Libraries add Babel plugins to the Metro pipeline to achieve the following transformation:

Before:

function foo(){
  'worklet'; // special directive to mark the function for transform
  console.log('hello world');
}

After (simplified):

function foo(){
  console.log('hello world');
}
foo.code = "function foo(){console.log('hello world');}"

Then, the code property of the function is passed to the target runtime and eval is invoked with the string as its argument.

Problems of the current approach

There are four key limitations with this approach:

Performance

The eval function is called with a string both in Debug and Release modes, meaning that there’s no way to benefit from Hermes AOT optimizations and use the bytecode. In some of my real-application benchmarks this made heavy-computation functions up to 5x as slow as the optimized ones. In general the gain is smaller but it’s definitely noticeable.

Bundle size

Extra strings of the functions’ code have to be included in the bundle and could make the bundle significantly bigger in size, especially in the case of long functions. This is also redundant in itself, since the bundle already contains the code, just not as a runtime string.

Debugging

The code running via eval strings has to be manually linked to source maps, which is both problematic and inaccurate, as the produced code may differ significantly from the original source code. Shared bundle would mean shared, accurate source maps with breakpoint support and transparent integration with the dev tools.

Using third party libraries

There’s no way to dispatch functions from other libraries on additional runtimes as they don’t contain their string code - for instance, it’s impossible to make web requests with axios since axios is not available on other runtimes.

The proposed solution solves all these problems.

New approach

If extra Hermes runtimes had the access to the JavaScript bundle of the application, there would be no need to append code strings to functions. The runtimes could evaluate the same bundle as React Native runtime does and require all the necessary functions that were dispatched to them with the module system provided by Metro.

The way I figured out is the least invasive way of implementing that is to expose the bundle for Turbo Modules the same way it’s done for TurboModuleRegistries. A Turbo Module could obtain a shared pointer to a const bundle after React Native Runtime receives it. Preferably the bundle could be accessible sooner. This is because in the initial evaluation of the bundle, the app might already try to utilize extra runtimes so they have to be ready to be set up by that time. However, it’s not of real importance now.

The key part of this approach is that the bundle is not copied nor fetched multiple times - reducing the impact on memory.

Caveats

Fast refresh is slightly problematic as it’s embedded in Metro and not easily integrated with. It would probably need a subscription mechanism or a separate TurboModule to forward it to other TurboModules. It could be implemented in pure JS but this could be slightly inconvenient for library authors as they’d need to subscribe in JS on top of implementing adding native code.

Evaluating the bundle on additional runtimes could negatively impact the start-up time of the application but in my experience the overhead is negligible and the evaluation could be executed on another thread, asynchronously, not using the UI or JS threads. This is also more of a problem of library authors (poor performance of their library) not React Native itself.

In the way the bundle works now, its evaluation of the bundle on an additional runtime results in an error. This is because of the require statements of React Native initializers and the app entry point itself at the end of the bundle. It’s easy to make a workaround for this but it’s not an ideal way of consuming the new API. I think this is something that we could iterate on in the future when we get the essentials ready.

Afterword

I’ve been dogfooding all proposed changes in the Reanimated repository for several months, implementing more features. I haven’t run into a single regression in React Native itself the changes would cause.

@tjzel
Copy link
Contributor Author

tjzel commented Oct 29, 2025

@huntie Could you take another look at it?

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants