Bridgeless mode: async JS "stacking up" when MainActivity is in the background
Description
Hi everyone, I'm encountering a really strange issue and hoping someone here might have some insight.
Here's the problem: In any new React Native app generated using @react-native-community/cli, when a secondary Activity is presented (and the main Activity is in the background), asynchronous JavaScript code seems to stop functioning correctly. It appears to "stack up" and only executes after the secondary Activity is dismissed.
For context, I'm developing a React Native package that has worked without this issue for the past two years. My local test app also works fine, and many users are using it successfully on modern React Native versions (I know of at least one on 0.76.7). This issue seems specific to freshly created apps.
I've created a minimal repo to reproduce the problem: https://github.com/descorp/RNAlertTest.
Interestingly, one user reported that turning off the "new Architecture" resolved this "thread freezing" problem for them, but I haven't been able to reproduce this fix locally.
My suspicion is that something has changed in the project generation process with @react-native-community/cli compared to the older npx react-native init, but I'm not sure what it could be.
Has anyone else experienced anything similar or have any ideas on what might be causing this? Any help would be greatly appreciated!
Steps to reproduce
- Clone https://github.com/descorp/RNAlertTest
- Run
yarn && yarn android - Press button to open dialog
- Press "Send event" on dialog and observe new messages console log
- Press "Send event async" on dialog and observe no new messages console log
- Dismiss dialog
- Observe debug console will all "stacked" async messages
Abstract:
- Initiate new project
- Add native module that presents activity above MainActivity (if there is a faster way, use it 😅).
- Open app and call new activity
- Call any async code (e.x.
await sleep(100)orfetch('www.google.com')) - Observe async code executed only after MainActivity on foreground
React Native Version
0.78.2
Affected Platforms
Runtime - Android
Output of npx @react-native-community/cli info
System:
OS: macOS 15.3.2
CPU: (16) arm64 Apple M4 Max
Memory: 185.73 MB / 64.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.11.0
path: /usr/local/adyen/nodejs/bin/node
Yarn: Not Found
npm:
version: 11.0.0-pre.0
path: /usr/local/adyen/npm/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.14.3
path: /Users/vladimir/.local/share/gem/ruby/3.2.0/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.1
- iOS 18.1
- macOS 15.1
- tvOS 18.1
- visionOS 2.1
- watchOS 11.1
Android SDK:
API Levels:
- "33"
- "34"
- "35"
Build Tools:
- 30.0.3
- 33.0.1
- 34.0.0
- 35.0.0
- 35.0.0
- 36.0.0
System Images:
- android-34 | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 16.1/16B40
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.6
path: /usr/bin/javac
Ruby:
version: 3.2.2
path: /usr/local/adyen/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.74.7
wanted: 0.74.7
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
Stacktrace or Logs
LOG AppState active
LOG
LOG Timer works before async
LOG Custom activity started
LOG AppState background
LOG Event sync
LOG Event sync
LOG AppState active
LOG Timer works after async
LOG Event async
LOG Event async
Reproducer
https://github.com/descorp/RNAlertTest
Screenshots and Videos
[!WARNING] Could not parse version: We could not find or parse the version number of React Native in your issue report. Please use the template, and report your version including major, minor, and patch numbers - e.g. 0.76.2.
[!WARNING] Could not parse version: We could not find or parse the version number of React Native in your issue report. Please use the template, and report your version including major, minor, and patch numbers - e.g. 0.76.2.
Hi @descorp thanks for the issue.
I highly doubt that the problem is related to the react-native-community/cli. The old react-native init was forwarding the initialization to react-native-community/cli under the hoods.
It is more likely that we changed something in the execution model and we "pause" the JS thread if we see that the activity is not active. I need to ask around as I don't have context about it.
Perhaps @cortinico or @rubennorte knows more about it.
Does it happens on Android only or also on iOS?
Does it happens on Android only or also on iOS?
Only on Android. On iOS we are using the root view controller for presentation. To draw an analogy, using a Fragment instead of an Activity works as expected since MainActivity is in the foreground and async JS code not "stacking".
Before you ask, rewriting the native library too use Fragments instead of Activity is not a feasible option 😅
It is more likely that we changed something in the execution model and we "pause" the JS thread if we see that the activity is not active.
I have noticed similar behaviour on Android when app is dismisses (which make sense). Could be that at some point App's lifecycle was changed to MainActivity lifecycle f?
It could be. I don't have a lot of details on the Android side, unfortunately. @cortinico can you have a look at this? I was asking internally and they told me that we always intended to pause JS execution when the Activity hosting React Native is not active, but it looks like that it was not implemented properly in the Old Arch, while it is in the New Arch... and now it's braking your library which was relying on that behavior... 😅
Thanks @cipolleschi
that it was not implemented properly in the Old Arch, while it is in the New Arch...
What grinds my gears is the fact, that disabling New Arch does not resolve it for me locally, and yet not stoping some library consumers with latest SDK to use it..
we always intended to pause JS execution when the Activity hosting
I understand the intent. Important question - will this eventually be backward compatible or I should start looking at some "background JS runner" solution?
I don't have an answer right now, unfortunately. I think we have to discuss this more thoroughly internally, to understand what will be the right execution model for this use case, and that's not a decision a can make myself. I'll keep this issue updated as soon as I learn more about this.
Thanks!
we always intended to pause JS execution when the Activity hosting
Just to emphasise, sync JS code works perfectly on background (you can see that on my reproducer repo). Async code is the problem.
I am not a JS core expert, but my assumption is that something is off with the way how Tasks are scheduled on event loop while Activity on a background.
i remember seeing something similar when there was an effort to make headlessJsTaskService to be newarch compatible. this PR ended up solving it. i dunno how practical it is to mess with the new timer manager. perhaps adding a persisting headlessJsTaskService will ensure the timer not getting paused and be a quick patch with proper clean up (RNTP works fine this way).
I've noticed setState also "stacks up" when MainAcitivty is backgrounded with new arch. I have no real solution other than pausing it all together.
Thanks for the tip @lovegaoshi I also see this issue that seems related.
As far as I understand, problem is the setTimeout it is not ticking while on background because of HeadlessJsTaskContext limitation ?
Again, what grinds my gears is the fact, that setting newArchEnabled=false does not resolve it. Issue persist on 0.72 and 0.78 alike with and without newArchEnabled.
rather than a limitation, I see all of this working as intended - jstimer's onHostPause is functioning as expected and paused all async execution I'm presuming, unless there are active headlessJsTasks. sync js code executes just fine. I dont see this is a new vs old arch thing either. perhaps you can find something by diffing your working test app with a RN template at the time, it shouldnt work to begin with
unfortunately i dont have any experience on this without involving headlessJsTaskService. the other pointer I have is this seemingly setState "stacking" issue only with the new arch, but since u mentioned neither arch work, i doubt this is a real lead.
Okay, I've investigated further and believe I've identified the underlying causes:
Following behaviour reproduced on blank new 0.78.2 app:
setTimer()do not work while on background for both "old" and "new arch". I shoot myself in a foot assuming that a simpleasync/awaitwithsetTimeoutwould be a reliable way to "reproduce" asynchronous behavior. Thanks @lovegaoshi for highlighting this area!new Promise(resolve => ... )only work on background in a "bridge" mode (bridgelessEnabled = falseornewArchEnabled=false)
This observation aligns with why the majority of users haven't experienced issues (likely using older RN versions or configurations) and why switching to the "old architecture" resolves the problem for those who do.
@cipolleschi does this behavior align with the React Native community's vision for background task execution, or should either of these points be considered for future improvement or clarification? (in other words "bug or feature?")
I really have to defer this to @cortinico as he has more experience on Android than me.
@cortinico
Just a follow-up. Is there any new information/better understanding of what is going on?
@descorp not yet, but we want to prioritize the issue. Bear with us for a little more! 🙏
Hey @descorp I've tried to reproduce your issue here:
- https://github.com/facebook/react-native/pull/51052
But the behavior is the same between old and new architecture. Can I ask you to try it as well and confirm you're seeing the same behavior between old and new arch?
Hey @cortinico
Thanks for looking into this!
But the behavior is the same between old and new architecture.
Sorry, the code you have copied to your reproducer is using setTimeout that causing troubles on it's own.
You can replace it with fetch, for example:
function sleep(ms: number) {
return new Promise(async resolve => {
await fetch("https://google.com");
resolve(1)
});
}
To be specific, the difference is not between new\old arch, but weather or not a "bridgeless" mode is enabled.
To be specific, the difference is not between new\old arch, but weather or not a "bridgeless" mode is enabled.
Toggling newArchEnabled=true or =false effectively turns on and off bridgeless mode as well. From my test, it seems like the behavior is the same with bridgeless/newarch ON or OFF.
behavior is the same
- can you confirm "stacking up" events till MainActivity is back on foreground ?
- Is this consistent if you get rid of
setTimeout?
TBH, I haven't figured how to run reproducer you have shared above. However I can reproduce "stacking" on 0.79.2 and 0.77.2 blank project(s).
I can share a .patch file so you can play around with blank project.
@descorp A patch would be great. You can also clone the react-native repository and create a PR modifying the RNTesterPlayground.js file. RNTester is a tester app we use to test react-native internally. Instructions to run it are here, although they might be a bit outdated.
- can you confirm "stacking up" events till MainActivity is back on foreground ?
Yes correct
- Is this consistent if you get rid of
setTimeout?
I will try and let you know.
In the meantime, if you can get your environment working up to be able to play with the reproducer I shared https://github.com/facebook/react-native/pull/51052 would be better.
You can find instruction about how to do it here: https://reactnative.dev/contributing/how-to-report-a-bug#rntesterplaygroundjs
A .patch file could also be helpful but given the nature of this bug, having the reproducer inside RNTEster would be better
I am attaching .patch file.
npx @react-native-community/cli@^18.0.0 init rn_reproducer --version 0.79.2 --install-pods false TestProject
cd rn_reproducer
git apply ../reproducer_79.patch
yarn install
yarn android
Will look into setting up RNTester
Hey @cortinico @cipolleschi,
Unfortunately, I wasn't able to run reproducer #51052 successfully.
More details
When using react-native-cortinico, I'm encountering multiple build errors related to hermes:
/react-native-cortinico/packages/react-native/sdks/hermes/API/hermes/../hermes/hermes.h:52:50: error: expected class name
/react-native-cortinico/packages/react-native/sdks/hermes/API/hermes/../hermes/hermes.h:54:25: error: no type named 'UUID' in namespace 'facebook::jsi'
...
fatal error: too many errors emitted, stopping now [-ferror-limit=]
C/C++: 20 errors generated.
> Task :packages:react-native:ReactAndroid:hermes-engine:buildCMakeRelease[arm64-v8a][libhermes] FAILED
However, I was able to reproduce the issue in this PR.
You can see in the DevTools log that the behavior changes when load(bridgelessEnabled = false) is used:
To reproduce:
- tap "OPEN ALERT"
- tap "SEND EVENT" couple of times
- tap "SEND ASYNC EVENT" couple of times
- tap "DISMISS"
Please let me know if I can present it in a more digestible way.
Hey @cortinico @cipolleschi
Just want to check on what would you recommend here:
Can we expect this to be fixed? Any workarounds we can try?
Just want to check on what would you recommend here: Can we expect this to be fixed? Any workarounds we can try?
I don't have an ETA to share. It's in my (sadly really long) list of bugs to look into. If someone from the community can look into this and suggest a Pull Request, I can make sure this gets prioritized accordingly.
Thanks @cortinico
I have spotted this article :
Starting from React Native 0.76, Turbo Native Modules have a new, type-safe API to emit events from the Native layer to the JS layer.
Any chance that switching to Turbo modules can improve "background" execution of JS?
Hey @descorp I spent some time investigating this one and have some updates for you.
tl;dr: the app is behaving as expected.
- So I run your reproducer with the Dialog and the 2 buttons. The version of it using
setTimerhas the behavior you mentioned. ThesetTimerwon't execute till the React Native Activity is back in the foreground.
That's due to the behavior in JavaTimerManager which is pausing all the timer execution when the ReactHost invokes onPause/onDestroy.
This is the expected behavior as otherwise animations, and rendering will continue even when the Activity is not on screen anymore.
Also the behavior is the same between old and new arch, so I can't find anything for us to fix here.
- I've also tried the variant you mentioned here: https://github.com/facebook/react-native/issues/50327#issuecomment-2858687968 using
fetchinstead ofsetTimerand it behaves correctly (i.e. things are executing when you click either of the two buttons) on both Old and New Arch.
So also here I don't know what else we're supposed to fix.
Please let me know if there is more that we should look into. I'll be closing this for now.
Hey @cortinico
Thanks for checking! Have you tried this reproducer: https://github.com/facebook/react-native/pull/51394 ?
You should be able to see in the DevTools log that the behavior changes when load(bridgelessEnabled = false/true) is used:
bridgelessEnabled = falsematches behavior on Old architecture.bridgelessEnabled = true(default) cause "queuing" of all async calls untilReactHostbackonResume.
setTimer
This is the expected behavior as otherwise animations, and rendering will continue even when the Activity is not on screen anymore.
I would like to challenge this statement. In my example the MainActivity is still visible in the background (and expected to respond to events)— it's onPause, but still on screen. I understand that it's hard to draw a precise line between these states on Android, but perhaps an option to manually tell the ReactActivity to "stay active" would be a nice addition.
Also the behavior is the same between old and new arch, so I can't find anything for us to fix here.
Fair, that part works exactly as it used to.
Just pinging to keep it alive :)
I would like to challenge this statement. In my example the MainActivity is still visible in the background (and expected to respond to events)— it's
onPause, but still on screen. I understand that it's hard to draw a precise line between these states on Android, but perhaps an option to manually tell theReactActivityto "stay active" would be a nice addition
To answer this one: you can use the HeadlessJsTaskContext.startTask(...) API to start a task. That will prevent the timer from pausing when your activity invokes the onPause.
I understand what you're saying, that you're activity is not on the foreground but it's still visible. When we implemented the jstimers back then, we decided to pause them when the Activity invokes the onPause callback.
In theory we could change this to be the onStop (which is what you're asking for). However the problem is that this would be a breaking change and there might be app out there still relying on this behavior. So using the HeadlessJsTaskContext API is probably your best solution here.