-
Notifications
You must be signed in to change notification settings - Fork 25k
feat: exposing JS Bundle for Native Modules #53928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to "BundleWrapper.cpp".
## Summary Based on changes from facebook/react-native#53928 ## Test plan Bundle Mode works for Android and iOS.
@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 😅 |
|
@huntie Sure! This is an (updated) proposal on the feature I made a while back: MotivationJavaScript multi-threading tools in React Native available now - namely WhyMultiple libraries like Current approachLibraries 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 approachThere are four key limitations with this approach: PerformanceThe 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 sizeExtra 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. DebuggingThe 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 librariesThere’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 approachIf 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. CaveatsFast 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. AfterwordI’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. |
|
@huntie Could you take another look at it? |
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 = _bundleProviderwith 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
getBundlemethod that returns the wrapper for the Bundle. There's alreadygetSourceURLmethod.Common
To facilitate these changes I had to change the way the Bundle is passed to
ReactInstance.cpp, but only slightly. Instead of passingstd::unique_ptr<const JSBigString>as the bundle I'm passingconst std::shared_ptr<const BigStringBuffer>. However, this change isn't significant asReactInstance.cppmade the buffer from the JSBigString immediately after receiving it.Possible improvements
It would be even better to distribute the Bundle as a result of
prepareJavaScriptJSI 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 = _bundleProviderto 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 inJReactInstance.cppto see that it's defined.