Replies: 41 comments
-
@zamotany, Thanks for writing this very detailed proposal, this is awesome. I think I got most of what you are proposing, so here are some questions I had. Also, please correct me if some of the assumptions in my questions are incorrect. As I understand, you are proposing this architecture for RN production apps, right ? Are you proposing this architecture for every single RN app, or more for super apps ? While I understand that this will greatly help super apps, how would it help standalone react native apps. Those apps will have to load the host, and the DLL parts, in addition to the actual bundle. With inline requires and RAM bundles, we could already evaluate and run code lazily - how else will this work ? |
Beta Was this translation helpful? Give feedback.
-
Yes. All RN apps would be running on this architecture, however decision whether to leverage multi-bundle feature is up to the developer and it's opt-in - it will be up to developer, if they want to use Host/DLL/apps bundles or a single regular/RAM bundle.
Not necessarily, as I mentioned in Implementation breakdown, the trigger for loading additional bundles (DLL/app) is coming from JS, so if the Host bundle is actually just a regular bundle (non-RAM/RAM), with application code, then nothing else will ever be loaded, since there's no code for that and the Host bundle will effectively become a normal application bundle (non-RAM/RAM) - the bundle we're all using currently.
Inline requires and RAM bundles does help with reducing TTI/TTR, however putting everything in single RAM bundle doesn't scale well. |
Beta Was this translation helpful? Give feedback.
-
I think my question is - can this not be done today with the unbundle (RAM bundle command), where the files are split ? the "import" command sort of already does this, right ? Sorry, I am having trouble understanding the difference. |
Beta Was this translation helpful? Give feedback.
-
The three biggest differences that I think are:
|
Beta Was this translation helpful? Give feedback.
-
@karanjthakkar there's already PR to fix Indexed RAM bundles on Androrid: facebook/react-native#24967 |
Beta Was this translation helpful? Give feedback.
-
@zamotany This is super well thought! Good job! I've been wanting for something like this since I did this lazy bundling experiment last year. If you want to chat about implementing this from the iOS side, I'd be happy to help! One of the questions that I have after reading this is, do you expect Haul to be the default bundler/packager if this were to be baked into RN? I don't know if metro supports this kind of code splitting atm.
Wow! Thank you! I'm going to give this a try internally and see how it works out! |
Beta Was this translation helpful? Give feedback.
-
@karanjthakkar Not really, for an average RN developer dealing with Haul and using this architecture might not be worth it. The main target audience here are big companies, where ability to scale easily is crucial. Webpack ecosystem has it's own shortcomings, so I don't see Haul as a replacement of Metro but rather an an alternative - for developers/companies whom will actually benefit from Haul + multi-bundles. |
Beta Was this translation helpful? Give feedback.
-
@zamotany Got it. So folks who want to use multi-bundles will essentially need to use Haul is what I wanted to confirm. Another question, how does the native side decide if it needs to run in multi bundle mode? Does the host bundle have some identifier to indicate that? |
Beta Was this translation helpful? Give feedback.
-
@karanjthakkar Unless the Host bundle requests to load additional bundles (DLL/app) from JS, it's basically operating in single-bundle mode, so no - there's no need for any additional identifier. In other words: by default it's running in single-bundle mode, when the trigger to load new bundle is received from JS, then it's running in multi-bundle mode. |
Beta Was this translation helpful? Give feedback.
-
Thanks @axemclion for your questions! Full disclosure, this architecture initially came out of some internal work I did, and @matthargett and I have been working with Callstack to build an implementation of this architecture. For standalone RN apps, the intention is that they would still be able to be deployed as a single RAM bundle, without any changes to how they’re deployed. I think your mention of super apps is a great example of where this could provide benefit over a single RAM bundle. For super apps, the “mini apps” would be able to be built against a common DLL bundle that’s provided by the super app, and deployed independently. The main benefit being that mini apps would be able to be run in an independent JS context from the super app. The independent JS context can be pre-warmed with the host app and DLL bundles, before it’s known what mini app will be loaded next. When a mini app is loaded, only its app-specific code needs to be evaluated, removing the initialization time of the common dependencies from the TTI. Ideally this can bring the TTI time down to be on par with the duration of the animation used to transition to the mini app. Another example might be something like the Facebook app. I don’t know its internals at all, but let’s say hypothetically that Marketplace and Profile are separate bundles. Code that is common to both, such as react, react-native, data access libraries like relay, common telemetry libraries, etc could be moved into the DLL bundle and preloaded into the host app in the background. Once the user selected the Marketplace or Profile tab, just the code specific to that tab could be loaded, providing a middle ground between a cold boot of those tabs and having them completely preloaded. |
Beta Was this translation helpful? Give feedback.
-
We use something very similar in Office. Boot perf is one benefit, although it would possibly be able to be optimized by a perfect bundle delay load solution instead. But another benefit of the separate instance solution is the ability to shut down the "mini apps". Using the Facebook app example, assuming Marketplace and Profile are both RN solutions. As you navigate around the Facebook app, loading more and more RN solutions, more and more of the bundle would get parsed and loaded into the instance. So once you've ever navigated to both the Marketplace and Profile, your instance would be forever the size of both those apps together. As the number of mini apps goes up, this becomes a massive memory hog. What we want is the ability after the user has navigated away from the MarketPlace, for the app to be able to shutdown the Marketplace app, and return to a memory usage based on just what the user is currently using. -- Otherwise RN features become essentially massive memory leaks. Having at least part of the bundle be shared between the instances also allows for optimizations around memory mapped files that allows the disk read to be consolidated between those instances, for additional performance gains. |
Beta Was this translation helpful? Give feedback.
-
In our company, we also have a scenario very similar to what you are describing and our current app is already working in a multi-bundle scenario. It's really good to see our teams working on this scenario! We should work together to find a better solution to it. As a Bank, our App has more than 100 difference services on it: credit cards, insurance, loans, Financial Tracker, Payments and many others. Many of those services are developed and owned by different teams. Each one of these team have a different backlog and business priorities. These services/team were mini apps inside my super app. In the last months, my team came with a simple solution to overcome schedule conflict's and bugs from mini app 1 impact on mini app2. We gave each mini app a bundle and let them develop and deploy it on their own time. A wrote a medium post about it. On my post, I describe same problems that the face. In addition to those described there, loading time and the memory consumption as described by @acoates-ms were another problems. However, the app have being used by more 15 M users without big problems. After a couple months in production, we were planning to design a system similar to what @zamotany I want to endorse this suggestion and offer our help. I'm really down a Skype meeting. |
Beta Was this translation helpful? Give feedback.
-
@joeblynch @acoates-ms @oximer |
Beta Was this translation helpful? Give feedback.
-
@zamotany Why using a single JSContext would be better? If we use two JSContext, we could avoid JS bugs from App 0 to affect App1. In addition, we avoid some issues with privacy and sensitive data between bundles. It would be pretty similar to different tabs inside a browser. |
Beta Was this translation helpful? Give feedback.
-
@oximer Yeah, that's true. After talking with @joeblynch, we decided to revisit the proposed architecture to make it use multiple |
Beta Was this translation helpful? Give feedback.
-
@CaptainNic it's similar, the distinction is that the host and DLL bundles are able to be deployed separately and shared by multiple app bundles. Since they're shared, they can be preloaded before it's known what app bundle will be loaded next. |
Beta Was this translation helpful? Give feedback.
-
@joeblynch It will help partially, but won't solve all these issues. Imagine the host bundle is built in one mono-repo, and App A bundle is built in another repo. Both contain module
In this case, as long as the hash contains relative paths, |
Beta Was this translation helpful? Give feedback.
-
@zamotany It would be nice for the design to move more of the logic and decision-making to the bundlers. As you wrote -- "Those guiding principles you mentioned are more targeted towards bundlers, not this architecture" -- I would love for the design to shift the principles to the bundlers. On React Native's side, this could happen by keeping the API small (both number of functions and what those functions can do), down to maybe just a couple variants of Currently in the proposal, React Native is aware of bundles and startup code. I'm suggesting to follow a browser-like model (principle 2) and expose a few script execution functions that the JS can call. Then this starts to open up pathways towards user-space HMR (ex: webpack-dev-server), different forms of bundle splitting and code reuse, and other features that JS can implement without needing to change React Native. |
Beta Was this translation helpful? Give feedback.
-
@axemclion @karanjthakkar @joeblynch @acoates-ms @oximer @ide @matthargett I have updated the issue with new details to account for multiple |
Beta Was this translation helpful? Give feedback.
-
I have researched the
|
Beta Was this translation helpful? Give feedback.
-
The Apennine Architecture allows you to decide if you want to run all bundles on a single |
Beta Was this translation helpful? Give feedback.
-
This is really great! Few questions:
|
Beta Was this translation helpful? Give feedback.
-
It appears that the PR with the proposed implementation was sent before we reached consensus. I would like to propose a way forward for this RFC and a possible implementation. Current State
Consensus is important because:
The size of the PR is an issue because:
This process certainly has trade-offs and you may have opinions about how we could do things differently. I am trying to explain why it is currently hard for us to integrate large changes such as this and why they need to be broken down into fully-working isolated changes step-by-step. Moving ForwardI propose the following steps to unblock this work that have the highest chance of getting included into RN:
Proposed changesAs discussed here by @ide and privately with me and other FB employees we believe there could be a much less invasive first solution that will gives us most of the benefits of the proposed architecture and allowing others to build third-party modules around this area to make it even better. I propose:
(thanks @rickhanlonii for reviewing this) |
Beta Was this translation helpful? Give feedback.
-
To reiterate the point about Hermes...from @acoates-ms:
I believe Hermes gives us all this on Android and it sounds like there was some unfortunate timing between this RFC and our efforts to release Hermes. Because the bytecode is a read-only mmapped file, it can actually return pages of bytecode back to the OS when they are no longer needed (I believe the GC can also return pages to the OS after collecting and compacting). @amnn might have more to add (or correct). |
Beta Was this translation helpful? Give feedback.
-
Thank you @cpojer and @sahrens for your feedback! It sounds like Facebook has been thinking about how to do achieve TTI reduction similar to this, but that this approach we've taken is not the same as Facebook's ideas. Could you share some more specifics of what the solution you've been thinking about looks like? We want to make sure we can get it working for all the users of RN on platforms they currently deploy on, and hopefully also be able to give it to the community who will be using it without Hermes / on iOS for a while. I agree fully that bytecode bundles are preferable over RAM bundles. I see the architecture proposed in this RFC as being a technique that's used in addition to bytecode, with both implemented to reduce TTI, but in different and complementary ways. The primary target for this architecture is environments like super apps, in which multiple "mini apps" run in separate JS contexts, while sharing much of the same initialization process. Each mini app within the super app will need to stand up an RN app instance, and for example might need to initialize libraries for navigation, data access, telemetry, etc. Since this work is common to every mini-app, there's no need to wait to do this work until it's known what mini app will launch next. Instead we preinitialize these common dependencies in a JS context, and when a mini app is loaded, its app bundle is evaluated in this pre-warmed context. The app's root component can then be mounted into the waiting RN app instance. By using bytecode bundles instead of RAM bundles, we get the additional benefit that the preinitialization time and app bundle eval time are both reduced, by being able to skip the lexing and parsing work. In theory, if the JS VM in use supports heap snapshots, it may be even faster to load the pre-warmed context from a snapshot. @matthargett and I discussed the proposed changes to move this forward with @zamotany today, and we are in agreement that we'd like to keep the required changes to the core of RN as lean as possible. He plans to comment further on the implementation of these changes. |
Beta Was this translation helpful? Give feedback.
-
@joeblynch, could you help us understand more about your approach of super apps and if that approach is common in the industry? For context, I think us at Facebook would consider the Facebook mobile app a "super app" 😉 . It has hundreds (thousands?) of distinct feature sets, and we try to think about things similarly. For example, when someone goes to Marketplace, and then to Dating, Dating should start up as quickly as possible with as little overhead as possible, and Marketplace should be able to be evicted, returning memory back to the operating system. I'm sure your group is needing to make some different tradeoffs than we are for the Facebook app, so it might be helpful to have some more context on those tradeoffs in order to figure out how they impact approaches here. |
Beta Was this translation helpful? Give feedback.
-
@cpojer After some thinking with @joeblynch and @matthargett we have an idea how to move forward that would be easier for FB to approve: First of all, we would split the work between refactoring and features, meaning the first PR (or set of PRs) would refactor the bundle types and logic around them. We'll aim at making the changes as small as possible in terms on PR size and bytecode size increase, so that it would be feasible to be merged. After that, the feature works, as suggested, would improve upon existing Please, let us know what you and the RN team at FB think about that. If there're any suggestions or comments, please write them here. |
Beta Was this translation helpful? Give feedback.
-
@zamotany, @joeblynch, and @matthargett what do you think about @cpojer's proposal? |
Beta Was this translation helpful? Give feedback.
-
@rickhanlonii As for using
Please correct me if I misunderstood the proposal. As for the first point with bundling, there's already a quite a lot of work in bundling step. Even in original proposal and original PR, the native side didn't know anything except of the initial bundle. Everything else was done by JS or requested from JS. Right now, we would like to focus on getting the native side done, since we already have the bundling done in Haul for multi-bundle scenario. |
Beta Was this translation helpful? Give feedback.
-
@cpojer Is there any feedback from RN team on #127 (comment) and #127 (comment) ?? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
RFC: Apennine Architecture
The Apennine architecture builds up the mountain of JS dependencies that are common to any RN app running on the platform, providing a ready and waiting foundation from which any app can quickly run.
Apennine aims to build this mountain as high as possible, by not only pre-evaluating common JS dependencies, but also initializing the framework through the point of starting an empty application that can host any app's components.
Disclaimer: for simplicity we are referring only to Android implementation here.
Architecture breakdown
When using the Apennine architecture the app would consist of at least 3 bundles:
react
,react-native
) or shared logic packed together.Both DLL bundles and app bundles can be a regular JS bundle (
BasicBundle
) or a Indexed/File RAM bundle.This multi-bundle architecture would be available not only in release mode like RAM bundles, but also in development mode and served from packager server, to allow for easy JS debugging with Remote debugger for all of the bundles including Host bundle and DLL bundles.
BundleRegistry
The single point for holding all bundles and managing them would be in
BundleRegistry
(which itself would be stored inInstance
). Each bundle would be given it's ownBundleExecutionEnvironment
.BundleExecutionEnvironment
would containNativeToJsBridge
instance (withJsToNativeBridge
) as well asMessageQueueThread
and aJSIExecutor
, which would hold aJSContext
for given bundle.All bundles would be stored in a
std::vector<Bundle>
and have aBundle* initialBundle
inBundleExecutionEnvironment
, so thatBundleExecutionEnvironment
knows, which bundle to load first. From there, the loading of additional bundles will be triggered from JavaScript.BundleRegistry
would also expose an API to be used on the native side.Bundles overview
Formats
A JS source code can be stored in one of the following formats:
BasicBundle
- regular bundle format, where all JS is loaded evaluated at once (the results for runningmetro/haul bundle
command).IndexedRAMBundle
- Single-file RAM bundle, where all modules are stored in single file, but loaded and evaluated lazily with constant lookup time (the results for runningmetro/haul ram-bundle --indexed-ram-bundle
command).FileRAMBundle
- Multi-file RAM bundle, where all modules are split into separate filesmetro/haul ram-bundle --platform=android
.DeltaBundle
- Delta bundle used bymetro
.Each bundle format would derive from abstract
Bundle
class with the following public interface:makeJavaScriptApi
would be responsible for preparing the JS environment, providing globals (likenativeRequire
) etc.load
would evaluate bundle's source code in given runtime.Host bundle
Format:
BasicBundle
.Content: Runtime logic and the only point, where
AppRegistry.registerComponent
should be present. Once theappSwitch
event is received it should take new App's root component from globalBundleRegistry
and render it.Notes:
BundleRegistry.initializeHost();
before doing anything.DLL bundles
Format:
BasicBundle
/IndexedRAMBundle
/FileRAMBundle
.Content: Common dependencies and shared logic.
Notes: React Native app can use 0, 1 or more DLL bundles. It's up to the developer to deicide which amount of DLL is the best for their use case and properly split them.
App bundles
Format:
BasicBundle
/IndexedRAMBundle
/FileRAMBundle
.Content: Application logic, components with root component exported as default export:
Notes: React Native app can use 1 or more app bundles. It's up to the developer to deicide how to split those.
Backwards compatibility
Be default
BundleRegistry
would be running in single-bundle mode and support single-bundle application regardless of bundle format. CallingBundleRegistry.initializeHost()
from JavaScript effectively switches into multi-bundle mode. This means that the older apps or apps that don't use multi-bundle functionality should work the same.Single vs multi-
JSContext
modeEach App bundle and Host bundle would have it's own
JSContext
assigned. All of thoseJSContext
s would be in the same group to allow for cross-JSContext
communication in a synchronous manner. This is to ensure that when app bundle is loaded and it's root component is added toBundleRegistry
, the Host bundle running in HostJSContext
can obtain theJSValueRef
to the root component and render it.The trigger to load new bundle (DLL or app) will can be done from both JS and Native code and the developer can decide if they want to load bundle in current execution environment (same
JSContext
) or a different one, but calling appropriate function:If
isAppBundle
istrue
, it will notify the Host bundle in HostJSContext
(which can be the same asJSContext
to load bundle to ifloadInCurrentEnv
is used) to use new App root component and render it.WIP: figure out how to provide cross-
JSContext
communication ifJSContext
s are running on separate threads.Bundle creation on native side
On native side the decision which bundle format to choose would be made in implementation of the following functions:
CatalystInstanceImpl::jniLoadScriptFromFile
CatalystInstanceImpl::jniLoadScriptFromDeltaBundle
CatalystInstanceImpl::jniLoadScriptFromFile
CatalystInstanceImpl::jniLoadScriptFromAssets
Instead of creating
RAMBundleRegistry
,JSDeltaBundleClientRAMBundle
or passingJSBigString
would create an instance ofBasicBundle
/IndexedRAMBundle
/FileRAMBundle
/DeltaBundle
and move it asstd::unique_ptr<Bundle>
toInstance::loadBundle
, which would evaluate the given bundleBundleRegistry
.Note: Source URL would be stored in instance of
Bundle
for later retrieval.Bundle creation
To create Host, DLL and App bundles, we would use Webpack's
DllPlugin
andDllReferencePlugin
together some custom logic in Haul.Discussion points
At this point we need to gather as many feedback regarding the native implementation and architecture as possible. Those changes will affect the core functionality of React Native so it is crucial to make sure nothing is broken. There might be small nuances, that I'm not aware in
ReactCommon
orCxxReact
- if you know about any of those, please share it with us.Other open questions are:
Instance::initializeBridge
?JSContext
communication and data sharing in user space?Co-Authored with @dratwas and @joeblynch
cc: @matthargett @nparashuram @fkgozali @TheSavior @cpojer @kelset @thymikee @dratwas @grabbou @satya164
Beta Was this translation helpful? Give feedback.
All reactions