[Android,iOS][Fabric] StickyView `translateY` using `useAnimatedScrollHandler` is very laggy and not reliable on new architecture
đ Bug Report: StickyView performance issues (lagging/jumping) on Android with New Architecture (Fabric)
Description
So I know there's probably more issues on the same topic open already, but I think it can be helpful to have another detailed report.
When using a custom StickyView component implemented with react-native-reanimated hooks like useSharedValue, useAnimatedScrollHandler, useAnimatedStyle, the sticky behavior on Android with the New Architecture (Fabric) is significantly degraded. The view exhibits noticeable lagging and jumping, unlike the smooth performance observed on iOS.
Environment
{
"react-native": "0.73.4",
"react-native-reanimated": "3.7.15",
}
Architecture: React Native New Architecture (Fabric) enabled on Android.
Minimal Reproduction
The issue can be reproduced using the following minimal code structure, which mimics the PageContainer and StickyView components.
Snack Link: https://snack.expo.dev/@genesiscz/reanimated-stickyview-performance?platform=android
Components
StickyView:
const StickyView = ({ scrollPosition, offset, children }: { children: ReactNode; scrollPosition: SharedValue<number>; offset: number }) => {
const elementY = useSharedValue<number>(0);
const isVisible = useSharedValue(false);
const onLayout = useCallback((event: LayoutChangeEvent) => {
preciseLoggerFromWorklet(`!!!! onLayout StickyView: ${event.nativeEvent.layout.y}`);
runOnUI((y: number) => {
elementY.value = y;
isVisible.value = true;
})(event.nativeEvent.layout.y);
}, []);
const combinedAnimatedStyle = useAnimatedStyle(() => {
const stickyThreshold = elementY.value - offset;
const stickStart = Math.max(0, stickyThreshold);
const translateY = scrollPosition.value <= stickyThreshold ? 0 : scrollPosition.value - stickyThreshold;
// Tried with interpolateColor, but it lags even without the backgroundColor
const opacity = interpolate(scrollPosition.value, [stickStart - 40, stickStart], [0, 1], Extrapolation.CLAMP);
preciseLoggerFromWorklet(`!!!! StickyView: scrollPosition.value=${scrollPosition.value} elementY.value=${elementY.value} offset=${offset}`);
return {
zIndex: 100,
transform: [{ translateY }],
backgroundColor: `rgba(30,31,46,${opacity})`,
opacity: isVisible.value ? 1 : 0,
};
});
return (
<Animated.View onLayout={onLayout} style={combinedAnimatedStyle}>
{children}
</Animated.View>
);
};
Page component: (just to demonstrate how I use useAnimatedScrollHandler)
const Page = ({ children, scrollPosition }: { children: ReactNode; scrollPosition: SharedValue<number> }) => {
const scrollViewRef = useRef<Animated.ScrollView>(null);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
const { y } = e.contentOffset;
scrollPosition.value = y;
// In a real app, you might save scroll position here:
// scrollPositions.value = { ...scrollPositions.value, [routeName]: y };
},
});
const { height } = Dimensions.get("window"); // Use window height for padding simulation
return (
<KeyboardAvoidingView behavior="padding" style={{ flex: 1 }} enabled={Platform.OS === "ios"}>
<Animated.ScrollView
keyboardShouldPersistTaps="handled"
ref={scrollViewRef}
contentContainerStyle={{
position: "relative",
flexGrow: 1,
paddingTop: 80, // Simulate header height
paddingBottom: 100, // Simulate tab bar and bottom padding
}}
style={styles.scrollView}
contentInsetAdjustmentBehavior="never"
onScroll={scrollHandler}
scrollEventThrottle={16} // Changing/omiting this doesn't fix it
fadingEdgeLength={50} // Omitted as not critical for minimal reproduction
>
{children}
</Animated.ScrollView>
</KeyboardAvoidingView>
);
};
Actual Behavior
On Android devices/emulators with the New Architecture (Fabric) enabled, scrolling causes the StickyView component to exhibit significant lagging and jumping. The animation is not smooth, making the sticky effect visually jarring. The logs show frequent updates but the rendering does not keep up smoothly.
Example Android Logs (showing frequent updates, but visual lag):
sdk_gphone64_x86_64:
[19:36:05.941 (+17ms)] !!!! StickyView: scrollPosition.value=961.1428833007812 elementY.value=932.1904907226562 offset=50.28571319580078
sdk_gphone64_x86_64:
[19:36:05.958 (+17ms)] !!!! StickyView: scrollPosition.value=865.90478515625 elementY.value=932.1904907226562 offset=50.28571319580078
sdk_gphone64_x86_64:
[19:36:05.977 (+19ms)] !!!! StickyView: scrollPosition.value=772.1904907226562 elementY.value=932.1904907226562 offset=50.28571319580078
// ... many similar log entries ...
sdk_gphone64_x86_64:
[19:36:16.349 (+21ms)] !!!! StickyView: scrollPosition.value=1689.90478515625 elementY.value=932.1904907226562 offset=50.28571319580078
Expected Behavior
The StickyView component should smoothly follow the scroll position and stick to the top of the screen when its elementY position reaches the offset, similar to how it behaves on iOS. The animation should be fluid without visible lagging or jumping.
Example iOS Logs (showing smooth updates correlating with smooth visual behavior):
hone 16 Pro:
[19:37:46.619 (+17ms)] !!!! StickyView: scrollPosition.value=1477.6666666666667 elementY.value=932 offset=62
iPhone 16 Pro:
[19:37:46.635 (+16ms)] !!!! StickyView: scrollPosition.value=1472.3333333333333 elementY.value=932 offset=62
iPhone 16 Pro:
[19:37:46.652 (+17ms)] !!!! StickyView: scrollPosition.value=1467.3333333333333 elementY.value=932 offset=62
// ... many similar log entries ...
iPhone 16 Pro:
[19:37:48.336 (+34ms)] !!!! StickyView: scrollPosition.value=1325.3333333333333 elementY.value=932 offset=62
Notes
- Even though the logs for the useAnimatedStyle show very similar diff between them, the actual component is just jumping / not updating the style fast enough
- The issue seems specific to Android devices running with the New Architecture (Fabric).
- The problem is not resolved by adjusting
scrollEventThrottle. - The sticky logic itself (calculating
translateY) is performed in a worklet viauseAnimatedStyle, which should typically perform well. - The problem might be related to how layout events or shared value updates are processed and synchronized between the JS/UI/Native threads specifically on Android Fabric, potentially differing from the behavior on the Old Architecture or iOS.
Steps to reproduce
- Set up a React Native project with the specified versions, ensuring the New Architecture (Fabric) is enabled for Android.
- Copy the minimal reproduction code given in Snack
- Run the application on an Android device or emulator with Fabric enabled.
- Scroll the list content up and down, observing the behavior of the "This is a Sticky View" header.
- Compare the behavior on Android with the behavior on an iOS device/simulator to see the difference in smoothness.
Android vs iOS video
https://github.com/user-attachments/assets/687a3ba7-59fe-4789-ab59-52ac54ec50be
https://github.com/user-attachments/assets/b05f0a1b-43aa-46b0-9d30-80f3aad06edb
Snack or a link to a repository
https://snack.expo.dev/@genesiscz/reanimated-stickyview-performance?platform=android
Reanimated version
3.17.5
React Native version
0.77.2
Platforms
Android
JavaScript runtime
None
Workflow
React Native
Architecture
Fabric (New Architecture)
Build type
Debug app & dev bundle
Device
Android emulator, Real device
Host machine
macOS
Device model
Any
Acknowledgements
Yes
So I know there's probably more issues on the same topic open already
@genesiscz Thanks for submitting this issue. Just out of curiosity, have you tried any of the suggested solutions from other issues like this one https://github.com/software-mansion/react-native-reanimated/issues/6992#issuecomment-2642591436?
Unfortunatelly no, I wish they linked that as a javascript flag. I would love to try to experiment that if I had the time but the release deadline for the upgrade is way too tight now. We just decided to disable the StickyView on Android along with more animations just for Android because after 0.72.X -> 0.77.2, reanimated 3.5.X -> 3.17.5, both ios and Android old arch -> new arch, (even with the patch) we were having
- unresponsive touchables that were in the Animated.View with layout / exiting / entering animations
- keeping unmounted elements visible on another screen ("ghosting")
- Performance degradations with animations present
- Other weird behaviors such as the #7453
We replaced most of the Animated.Views with
import { Platform, View, ViewProps } from "react-native";
import Animated, { AnimatedProps } from "react-native-reanimated";
// React Native 0.77.2 + Reanimated 3.17.5 somehow broke the animations, so we need to disable them on Android
// See:
// - https://github.com/software-mansion/react-native-reanimated/issues/4534
// - https://github.com/software-mansion/react-native-reanimated/issues/7440
// - https://github.com/software-mansion/react-native-reanimated/issues/7109
// - https://github.com/software-mansion/react-native-reanimated/issues/4811
const ANIMATIONS_ALLOWED = Platform.OS === "ios";
const ALWAYS_RETURN_ANIMATED_VIEW = false;
export const ReanimatedView = (props: AnimatedProps<ViewProps>) => {
const { exiting, entering, layout, ...rest } = props;
const animationProps = ANIMATIONS_ALLOWED ? { exiting, entering, layout } : {};
if (ANIMATIONS_ALLOWED || ALWAYS_RETURN_ANIMATED_VIEW) {
return <Animated.View {...rest} {...animationProps} />;
}
return <View {...rest as ViewProps} />;
};
I will try to fill more bug reports after the release is done (althought sometimes it's not as easy as I wish to make the repros), which is in a week. Thank you so much for the work you've been putting into this library and I understand it's not easy to keep it all smooth. Thank you, @tomekzaw đ¯
@genesiscz Sorry to hear that đĸ Unfortunately, Reanimated heavily relies on some React Native internals (namely Fabric renderer and ShadowTree APIs) and even small changes on react-native side can cause chaos on our side, that's basically the root cause of some recent problems on the New Architecture. We really appreciate your detailed comments, we'll definitely try to fix it as soon as possible.
@tomekzaw I have to ask you one more important question, is there currently a way to get the correct stack trace for the Warnings about the .value being pulled from the JS thread? I tried the babel extension but on 0.77.2 all the LogBox's warnings are just redirected to Chrome Devtools menu which create a very useless trace which have only 9 frames (which is still too short to find the real file it's been coming from) and the babel plugin doesn't seem to do anything with the chrome traces :(
@genesiscz This is a known issue, I believe @MatiPl01 can say more about this.
@genesiscz
is there currently a way to get the correct stack trace for the Warnings about the .value being pulled from the JS thread?
Unfortunately, there's currently no way to get the correct stack trace (at least I don't know any).
We are aware of the problem and you are correct that it started happening after LogBox warnings were removed and the Chrome Debugger was introduced. This is a problem that we should tackle and solve in the near future because the stack trace doesn't show correct location of the warning and paths to the files aren't pointing to sources, which doesn't help while debugging.
I have no idea if this is related, but I got a notification from the xCode Regression analytics (iOS - I let the StickyView to be still sticky):
@genesiscz Would you be so kind and check if patching ShadowTree::commit method in ShadowTree.cpp in react-native fixes the problem for you? With this patch, we were able to fix laggy sticky header in the StickyHeader example in our fabric-example app on iOS which currently runs on [email protected] where enableSynchronousStateUpdates feature flag is already enabled by default. Please note that this is not considered a production-ready fix yet.
- Make sure that
enableSynchronousStateUpdatesfeature flag is already enabled. It is already enabled by default in RN 0.80, but you need to manually enable it in RN 0.79 (and probably older): https://github.com/facebook/react-native/blob/b338a004676ba67b90cfa4d52ff15650cd73bbfa/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp#L161-L163
bool ReactNativeFeatureFlags::enableSynchronousStateUpdates() {
- return getAccessor().enableSynchronousStateUpdates();
+ return true;
}
- Apply this change in
node_modules/react-native/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp: https://github.com/facebook/react-native/blob/b338a004676ba67b90cfa4d52ff15650cd73bbfa/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp#L235-L252
CommitStatus ShadowTree::commit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
// WARNING: This is *not* production-ready code. Please use it wisely.
// For more details, see https://github.com/software-mansion/react-native-reanimated/issues/7460.
size_t myTicket = ticketCounter.fetch_add(1, std::memory_order_relaxed);
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]() { return serviceCounter.load(std::memory_order_relaxed) == myTicket; });
CommitStatus status = tryCommit(transaction, commitOptions);
serviceCounter.fetch_add(1, std::memory_order_relaxed);
cv.notify_all();
return status;
}
- Apply this change in
node_modules/react-native/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h: https://github.com/facebook/react-native/blob/b338a004676ba67b90cfa4d52ff15650cd73bbfa/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h#L156-L157
private:
// ...
std::shared_ptr<const MountingCoordinator> mountingCoordinator_;
+ mutable std::mutex mtx;
+ mutable std::condition_variable cv;
+ mutable std::atomic<size_t> ticketCounter = 0;
+ mutable std::atomic<size_t> serviceCounter = 0;
};
- Apply this change in
node_modules/react-native-reanimated/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpp: https://github.com/software-mansion/react-native-reanimated/blob/e3ef6dafbe450814a45b8dbaef6ccb25084dcef3/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpp#L21-L23
void UpdatesRegistryManager::pauseReanimatedCommits() {
- isPaused_ = true;
}
@tomekzaw Can you kindly provide a quick nudge into how should I rebuild it with the new code? Is it as simple as just fixing the file in-place and re-building the app with react-native run-android --mode devDebug --active-arch-only? Also, do you mean I should try this in our app which runs on 0.77.x now? How would I apply the enableSynchronousStateUpdates flag and re-build the react-native then? Is that also as easy as just re-building the RN app? I will try to do my best to assist you with this since we're getting brutal regressions even on iOS and I know are doing the best to get this over with :-)
@genesiscz First you need to make sure that the version of react-native that you're using has enableSynchronousStateUpdates feature flag and if it's already enabled (it needs to be enabled).
Then you need to build the app from source. On iOS, react-native is always built from source (at least for now) so you can just edit the files in node_modules and rebuild the app.
On Android, you will need to enable build from source by following this guide: https://reactnative.dev/contributing/how-to-build-from-source (basically just add these lines of code (without the leading + characters) to your settings.gradle. Then build your app as usual from the CLI or via Android Studio.
If you're using Android Studio to build the app, please make sure that in Android Studio > Settings... > Build, Execution, Deployment > Build Tools > Gradle, in "Gradle JDK" you have selected "JAVA_HOME" (v17) instead of "GRADLE_LOCAL_JAVA_HOME" (v21).
Ok let's do this:
NativeReactNativeFeatureFlags.cpp
Just to be sure, is it okay to be on the 3.17.5 RNR right?
--- a/ShadowTree.cpp
+++ b/ShadowTree.cpp
@@ -1,18 +1,19 @@
CommitStatus ShadowTree::commit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
- [[maybe_unused]] int attempts = 0;
+ // WARNING: This is *not* production-ready code. Please use it wisely.
+ // For more details, see https://github.com/software-mansion/react-native-reanimated/issues/7460.
+ size_t myTicket = ticketCounter.fetch_add(1, std::memory_order_relaxed);
+ std::unique_lock<std::mutex> lock(mtx);
+ cv.wait(lock, [&]() { return serviceCounter.load(std::memory_order_relaxed) == myTicket; });
+ CommitStatus status = tryCommit(transaction, commitOptions);
+ serviceCounter.fetch_add(1, std::memory_order_relaxed);
+ cv.notify_all();
+ return status;
- while (true) {
- attempts++;
-
- auto status = tryCommit(transaction, commitOptions);
- if (status != CommitStatus::Failed) {
- return status;
- }
-
- // After multiple attempts, we failed to commit the transaction.
- // Something internally went terribly wrong.
- react_native_assert(attempts < 1024);
- }
}
** I WILL EDIT THIS COMMENT AS I PROGRESS **
@tomekzaw Do you wanna exchange some chat channel where can we do this interactively? Only if this helps you of course, in now way do I want you to debug this for me, but I am assuming it can help us both in the final resolution. You can DM me on X https://x.com/martin_foltyn
EDIT: I guess this won't work for RN 0.77, and I am not sure I will be able to run it on 0.80.0-rc-4, guessing the ShadowTree.cpp is quite different on 0.77 vs 0.80
@genesiscz I'm not sure if this will work on 3.17.5, we're using current main (or 4.0.0-beta.5 should also be fine). I'm not sure either if this will work with React Native 0.77, we're using 0.80.0-rc.4 from our current main branch.
ShadowTree.cpp is a bit different but this very method hasn't changed for at least a few versions so it should be easy to apply.
I also forgot to mention one thing. Please remove (or comment out) the following line isPaused_ = true in UpdatesRegistryManager.cpp as described in https://github.com/software-mansion/react-native-reanimated/issues/7460#issuecomment-2939474567.
Ok, let me quickly try upgrading to it, but our project is huge, so I am not sure how well will it go with other libraries. I went through the Upgrade helper already and upgraded to 4.0.0-beta.5 so I guess I will have answer in an hour, but our last upgrade from 0.72 to (somehow functioning) 0.77 took me about 250 hours. Let's see if I can do this in an hour đ
@genesiscz I'm not sure if this will work on
3.17.5, we're using current main (or4.0.0-beta.5should also be fine). I'm not sure either if this will work with React Native0.77, we're using0.80.0-rc.4from our current main branch.
ShadowTree.cppis a bit different but this very method hasn't changed for at least a few versions so it should be easy to apply.
Oh and for the difference, I got this from the ShadowTree modification
Right, so you'll also need to add these lines at the bottom of ShadowTree.h (inside private:) as described in https://github.com/software-mansion/react-native-reanimated/issues/7460#issuecomment-2939474567.
I am afraid this won't be as easy as I would like
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[7]'
*** First throw call stack:
(
0 CoreFoundation 0x00000001804b910c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x0000000180092da8 objc_exception_throw + 72
2 CoreFoundation 0x00000001805289d8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 660
3 CoreFoundation 0x00000001804b79b4 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 48
4 ColMobile UAT.debug.dylib 0x00000001097c5d20 __61+[RCTThirdPartyComponentsProvider thirdPartyFabricComponents]_block_invoke + 1440
5 libdispatch.dylib 0x0000000102b0a5d0 _dispatch_client_callout + 16
6 libdispatch.dylib 0x0000000102b0c088 _dispatch_once_callout + 84
7 ColMobile UAT.debug.dylib 0x00000001097c5758 +[RCTThirdPartyComponentsProvider thirdPartyFabricComponents] + 84
8 ColMobile UAT.debug.dylib 0x0000000109730388 -[RCTAppDependencyProvider thirdPartyFabricComponents] + 36
9 ColMobile UAT.debug.dylib 0x0000000109445240 -[RCTDefaultReactNativeFactoryDelegate thirdPartyFabricComponents] + 148
10 ColMobile UAT.debug.dylib 0x0000000109446ee0 -[RCTReactNativeFactory thirdPartyFabricComponents] + 108
11 ColMobile UAT.debug.dylib 0x00000001094a9378 -[RCTComponentViewFactory _registerComponentIfPossible:] + 460
12 ColMobile UAT.debug.dylib 0x00000001094afd30 _ZZZ54+[RCTComponentViewFactory currentComponentViewFactory]EUb_ENK3$_1clEPKc + 64
13 ColMobile UAT.debug.dylib 0x00000001094afce4 _ZNSt3__18__invokeB8de190102IRZZ54+[RCTComponentViewFactory currentComponentViewFactory]EUb_E3$_1JPKcEEEDTclclsr3stdE7declvalIT_EEspclsr3stdE7declvalIT0_EEEEOS5_DpOS6_ + 36
14 ColMobile UAT.debug.dylib 0x00000001094afcb4 _ZNSt3__128__invoke_void_return_wrapperIvLb1EE6__callB8de190102IJRZZ54+[RCTComponentViewFactory currentComponentViewFactory]EUb_E3$_1PKcEEEvDpOT_ + 32
15 ColMobile UAT.debug.dylib 0x00000001094afc88 _ZNSt3__110__function12__alloc_funcIZZ54+[RCTComponentViewFactory currentComponentViewFactory]EUb_E3$_1NS_9allocatorIS2_EEFvPKcEEclB8de190102EOS6_ + 36
16 ColMobile UAT.debug.dylib 0x00000001094af850 _ZNSt3__110__function6__funcIZZ54+[RCTComponentViewFactory currentComponentViewFactory]EUb_E3$_1NS_9allocatorIS2_EEFvPKcEEclEOS6_ + 36
17 ColMobile UAT.debug.dylib 0x0000000108cccffc _ZNKSt3__110__function12__value_funcIFddEEclB8de190102EOd + 68
18 ColMobile UAT.debug.dylib 0x00000001092693c4 _ZNKSt3__18functionIFvPN8facebook5react10JSExecutorEEEclES4_ + 36
19 ColMobile UAT.debug.dylib 0x00000001092692a8 _ZNK8facebook5react35ComponentDescriptorProviderRegistry7requestEPKc + 124
20 ColMobile UAT.debug.dylib 0x000000010926ccb4 _ZNK8facebook5react27ComponentDescriptorRegistry2atERKNSt3__112basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEE + 228
21 ColMobile UAT.debug.dylib 0x0000000109333c00 _ZNK8facebook5react9UIManager10createNodeEiRKNSt3__112basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEEiNS0_8RawPropsENS2_10shared_ptrIKNS0_14InstanceHandleEEE + 152
22 ColMobile UAT.debug.dylib 0x000000010934dd34 _ZZN8facebook5react16UIManagerBinding3getERNS_3jsi7RuntimeERKNS2_10PropNameIDEENK3$_0clES4_RKNS2_5ValueEPSA_m + 520
23 ColMobile UAT.debug.dylib 0x000000010934db20 _ZNSt3__18__invokeB8de190102IRZN8facebook5react16UIManagerBinding3getERNS1_3jsi7RuntimeERKNS4_10PropNameIDEE3$_0JS6_RKNS4_5ValueEPSD_mEEEDTclclsr3stdE7declvalIT_EEspclsr3stdE7declvalIT0_EEEEOSG_D 24 ColMobile UAT.debug.dylib 0x000000010934dacc _ZNSt3__128__invoke_void_return_wrapperIN8facebook3jsi5ValueELb0EE6__callB8de190102IJRZNS1_5react16UIManagerBinding3getERNS2_7RuntimeERKNS2_10PropNameIDEE3$_0S9_RKS3_PSF_mEEES3_DpOT_ + 64
25 ColMobile UAT.debug.dylib 0x000000010934da80 _ZNSt3__110__function12__alloc_funcIZN8facebook5react16UIManagerBinding3getERNS2_3jsi7RuntimeERKNS5_10PropNameIDEE3$_0NS_9allocatorISB_EEFNS5_5ValueES7_RKSE_PSF_mEEclB8de190102ES7_SG_OSH_Om + 72
26 ColMobile UAT.debug.dylib 0x000000010934d000 _ZNSt3__110__function6__funcIZN8facebook5react16UIManagerBinding3getERNS2_3jsi7RuntimeERKNS5_10PropNameIDEE3$_0NS_9allocatorISB_EEFNS5_5ValueES7_RKSE_PSF_mEEclES7_SG_OSH_Om + 68
27 hermes 0x0000000103b97f80 _ZN8facebook6hermes17HermesRuntimeImpl9HFContext4funcEPvRN6hermes2vm7RuntimeENS5_10NativeArgsE + 532
28 hermes 0x0000000103c50a24 _ZN6hermes2vm14NativeFunction11_nativeCallEPS1_RNS0_7RuntimeE + 144
29 hermes 0x0000000103c6acb0 _ZN6hermes2vm11Interpreter17interpretFunctionILb0ELb0EEENS0_10CallResultINS0_11HermesValueELNS0_6detail20CallResultSpecializeE2EEERNS0_7RuntimeERNS0_16InterpreterStateE + 2320
30 hermes 0x0000000103c6a36c _ZN6hermes2vm7Runtime21interpretFunctionImplEPNS0_9CodeBlockE + 132
31 hermes 0x0000000103c50cf8 _ZN6hermes2vm10JSFunction9_callImplENS0_6HandleINS0_8CallableEEERNS0_7RuntimeE + 40
32 hermes 0x0000000103c50508 _ZN6hermes2vm13BoundFunction10_boundCallEPS1_PKNS_4inst4InstERNS0_7RuntimeE + 504
33 hermes 0x0000000103b900d4 _ZN8facebook6hermes17HermesRuntimeImpl4callERKNS_3jsi8FunctionERKNS2_5ValueEPS7_m + 752
34 ColMobile UAT.debug.dylib 0x0000000108ccbe28 _ZNK8facebook3jsi8Function4callERNS0_7RuntimeEPKNS0_5ValueEm + 100
35 ColMobile UAT.debug.dylib 0x0000000108ccbd38 _ZNK8facebook3jsi8Function4callERNS0_7RuntimeESt16initializer_listINS0_5ValueEE + 112
36 ColMobile UAT.debug.dylib 0x000000010972e888 _ZN8facebook5react4Task7executeERNS_3jsi7RuntimeEb + 276
37 ColMobile UAT.debug.dylib 0x0000000109721d70 _ZNK8facebook5react23RuntimeScheduler_Modern11executeTaskERNS_3jsi7RuntimeERNS0_4TaskEb + 148
38 ColMobile UAT.debug.dylib 0x0000000109722aa8 _ZN8facebook5react23RuntimeScheduler_Modern16runEventLoopTickERNS_3jsi7RuntimeERNS0_4TaskENSt3__16chrono10time_pointINS8_12steady_clockENS8_8durationIxNS7_5ratioILl1ELl1000000000EEEEEEE + 260
39 ColMobile UAT.debug.dylib 0x0000000109722690 _ZN8facebook5react23RuntimeScheduler_Modern12runEventLoopERNS_3jsi7RuntimeEb + 252
40 ColMobile UAT.debug.dylib 0x000000010972626c _ZZN8facebook5react23RuntimeScheduler_Modern17scheduleEventLoopEvENK3$_0clERNS_3jsi7RuntimeE + 44
41 ColMobile UAT.debug.dylib 0x0000000109726234 _ZNSt3__18__invokeB8de190102IRZN8facebook5react23RuntimeScheduler_Modern17scheduleEventLoopEvE3$_0JRNS1_3jsi7RuntimeEEEEDTclclsr3stdE7declvalIT_EEspclsr3stdE7declvalIT0_EEEEOS9_DpOSA_ + 32
42 ColMobile UAT.debug.dylib 0x0000000109726208 _ZNSt3__128__invoke_void_return_wrapperIvLb1EE6__callB8de190102IJRZN8facebook5react23RuntimeScheduler_Modern17scheduleEventLoopEvE3$_0RNS3_3jsi7RuntimeEEEEvDpOT_ + 32
43 ColMobile UAT.debug.dylib 0x00000001097261dc _ZNSt3__110__function12__alloc_funcIZN8facebook5react23RuntimeScheduler_Modern17scheduleEventLoopEvE3$_0NS_9allocatorIS5_EEFvRNS2_3jsi7RuntimeEEEclB8de190102ESA_ + 36
44 ColMobile UAT.debug.dylib 0x0000000109725dc4 _ZNSt3__110__function6__funcIZN8facebook5react23RuntimeScheduler_Modern17scheduleEventLoopEvE3$_0NS_9allocatorIS5_EEFvRNS2_3jsi7RuntimeEEEclESA_ + 36
45 ColMobile UAT.debug.dylib 0x0000000108cccffc _ZNKSt3__110__function12__value_funcIFddEEclB8de190102EOd + 68
46 ColMobile UAT.debug.dylib 0x0000000108e31690 _ZNKSt3__18functionIFvRN8facebook3jsi7RuntimeEEEclES4_ + 32
47 ColMobile UAT.debug.dylib 0x00000001095f42f4 _ZZZN8facebook5react13ReactInstanceC1ENSt3__110unique_ptrINS0_9JSRuntimeENS2_14default_deleteIS4_EEEENS2_10shared_ptrINS0_18MessageQueueThreadEEENS8_INS0_12TimerManagerEEENS2_8functionIFvRNS_3jsi 48 ColMobile UAT.debug.dylib
libc++abi: terminating due to uncaught exception of type NSException
I will try to do another try with RN 0.77 with RNR 4.0.0 the changes suggested and let you know
@genesiscz This is a known issue in react-native-codegen. As you can see, the key-value pair for RNSScreenStackHeaderConfig (and RNSScreen) is corrupted. You need to bump react-native-screens to the latest version or manually move RNSScreenStackHeaderConfigCls and RNSScreenCls functions at the bottom of the file to fix this.
Bump to the nightly RNS didn't help either, it still has the corrupted config... did only that bump to nightly work? 4.11.1-nighly-20250603
@genesiscz Hmm, in that case please try moving Cls functions at the bottom of the file.
@tomekzaw So
TLDR: 0.80 failed, 0.77 managed to upgrade the packages, not able to try the specific screen that is causing the most problems, but tried to reproduce it elsewhere with success. Scroll down for the reproduction :)
0.80
> 0.80 failed horribly, just gave up:
I moved the functions at the bottom, ran pod install and re-built, but this is as far as I got...
BUNDLE ./AppWithSentry.tsx
ERROR ../node_modules/htmlparser2/lib/esm/index.js: /<proj>/node_modules/htmlparser2/lib/esm/index.js: Export namespace should be first transformed by `@babel/plugin-transform-export-namespace-from`.
46 | * They should probably be removed eventually.
47 | */
> 48 | export * as ElementType from "domelementtype";
| ^^^^^^^^^^^^^^^^
49 | import { getFeed } from "domutils";
50 | export { getFeed } from "domutils";
51 | const parseFeedDefaultOptions = { xmlMode: true };
WARN The package /<proj>/packages/col-business/common/shared contains an invalid package.json configuration. Consider raising this issue with the package maintainer(s).
Reason: The resolution for "/<proj>/packages/col-business/common/shared" defined in "exports" is /<proj>/packages/col-business/common/shared/src, however this file does not exist. Falling back to file-based resolution.
ERROR ../node_modules/htmlparser2/lib/esm/index.js: /<proj>/node_modules/htmlparser2/lib/esm/index.js: Export namespace should be first transformed by `@babel/plugin-transform-export-namespace-from`.
46 | * They should probably be removed eventually.
47 | */
> 48 | export * as ElementType from "domelementtype";
| ^^^^^^^^^^^^^^^^
49 | import { getFeed } from "domutils";
50 | export { getFeed } from "domutils";
51 | const parseFeedDefaultOptions = { xmlMode: true };
INFO Connection closed to device='iPhone 16 Pro' for app='cm' with code='1006' and reason=''.
BUNDLE ./index.js
ERROR ../node_modules/htmlparser2/lib/esm/index.js: /<proj>/node_modules/htmlparser2/lib/esm/index.js: Export namespace should be first transformed by `@babel/plugin-transform-export-namespace-from`.
46 | * They should probably be removed eventually.
47 | */
> 48 | export * as ElementType from "domelementtype";
| ^^^^^^^^^^^^^^^^
49 | import { getFeed } from "domutils";
50 | export { getFeed } from "domutils";
51 | const parseFeedDefaultOptions = { xmlMode: true };
unfortunatelly I did not get past this in any way... I guess it won't be possible to just "make it work" in a few hours on such a big project without bumping a lot of packages. Most of the packages have been upgraded to the latest possible version so most of them should probably have it sorted out, but it is possible I would have to bump them all again and hope it works :( My babel.config.js is
module.exports = {
presets: [["module:@react-native/babel-preset"]],
plugins: [
["react-native-worklets-core/plugin"],
["react-native-worklets/plugin"],
[
"@babel/plugin-transform-react-jsx",
{
runtime: "automatic",
},
],
[
"module-resolver",
{
root: [".."],
extensions: [".ts", ".tsx", ".js", ".jsx"],
alias: {
...
},
},
],
],
};
0.77
I tried to make it work on the 0.77 with RNR 4.0.0-beta5, react-native-worklets 0.3.0, react-native-gesture-handler 2.25.0, react-native-screens, even tried some of your patches from the .yarn directory for the libraries but unfortunatelly we are heavily dependent on react-native-ui-lib which hangs on reanimated 3.16.7 for now causing the conflict
My babel.config.js is the same as in previous try
**Try on the worst page failed**
The problem is that the most problematic page is exactly where the TabController is heavily used:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Warning: TypeError: Cannot read property '_reactInternals' of null
This error is located at:
in GestureDetector (created by TabBarItem)
in TabBarItem (created by TabControllerTabBarItem)
in TabControllerTabBarItem (created by TabController.TabBar)
After that it just bloats the memory of the app to 7.67GB and hangs:
I even tried forcing resolutions:
"resolutions": {
"react-native": "^0.77.2",
"react-native-reanimated": "4.0.0-beta.5",
"react-native-worklets": "0.3.0"
},
But that didn't help.
So I made a reproduction on more basic but duplicated a few components to make it longer. Of course, every step you wanted me to do I changed.
warning: I had changed the feature flag in both NativeReactNativeFeatureFlags.cpp and ReactNativeFeatureFlags.cpp to return true in case that was a wrong thing to do
But you know what? It doesn't matter. I tried to just make the DashboardScreen pretty heavy with some of the components from the problematic page, added the StickyView and added the FadedScrollView (which makes it much worse to keep it sticky IMO) - The FadedScrollView is basically this https://github.com/wix/react-native-ui-lib/blob/653cdd509f420ea6c2d4a1b662b745d985370de6/src/components/fadedScrollView/index.tsx#L72
Video: video
StickyView:
<StickyView scrollPosition={scrollPosition} offset={headerHeight + extraHeight}>
<FadedScrollView forwardedRef={null}>
<View style={{ height: stickyTabBarHeight, backgroundColor: "red", width: "100%" }}>
<Text>Test</Text>
</View>
</FadedScrollView>
</StickyView>
If I remove the FadedScrollView it's a bit better, but still so bad:
If you REALLY want to make sure I don't do anything stupid along the way:
Just to make sure here's the ACTUAL code StickyView:
import { ReactNode, useCallback } from "react";
import { LayoutChangeEvent } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
interpolate,
runOnUI,
Extrapolation,
} from "react-native-reanimated";
import { SharedValue } from "react-native-reanimated";
interface StickyProps {
scrollPosition: SharedValue<number>;
offset?: number;
children: ReactNode;
}
export const StickyView = ({ children, scrollPosition, offset = 0 }: StickyProps) => {
const elementY = useSharedValue<number>(0);
const isVisible = useSharedValue(false);
const { log } = useLogger("UI", "StickyView");
const onLayout = useCallback((event: LayoutChangeEvent) => {
log.debug(`onLayout: ${event.nativeEvent.layout.y}`);
runOnUI((y: number) => {
elementY.value = y;
isVisible.value = true;
})(event.nativeEvent.layout.y);
}, []);
const combinedAnimatedStyle = useAnimatedStyle(() => {
const stickyThreshold = elementY.value - offset;
const stickStart = Math.max(0, stickyThreshold);
const translateY = scrollPosition.value <= stickyThreshold ? 0 : scrollPosition.value - stickyThreshold;
const opacity = interpolate(
scrollPosition.value,
[stickStart - 40, stickStart],
[0, 1],
Extrapolation.CLAMP,
);
return {
zIndex: 100,
transform: [{ translateY }],
backgroundColor: `rgba(30,31,46,${opacity})`,
opacity: isVisible.value ? 1 : 0,
};
});
return (
<Animated.View onLayout={onLayout} style={[combinedAnimatedStyle]}>
{children}
</Animated.View>
);
};
scrollPosition comes from useScrollPositionForRoute:
import { useRoute } from "@react-navigation/native";
import { usePageScrollContext } from "./PageScrollContext";
import { useDerivedValue } from "react-native-reanimated";
export const useScrollPositionForRoute = () => {
const { scrollPositions } = usePageScrollContext();
const route = useRoute();
const routeName = route.name;
const scrollPosition = useDerivedValue(() => {
return scrollPositions.value[routeName] ?? 0;
}, [routeName]);
return scrollPosition;
};
I update the scrollPositions just like this in Page.tsx (any screen's wrapper):
const { scrollY, scrollPositions } = usePageScrollContext();
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
const { y } = e.contentOffset;
logFromWorklet.silly(`scrollHandler - contentOffset: ${JSON.stringify(e.contentOffset)}`);
scrollY.value = y;
scrollPositions.value = {
...scrollPositions.value,
[route.name]: y,
};
},
});
There's only one place where I act on the scrollPositions - which is also visible - the App header and its blur styling:
import { BlurView } from "@react-native-community/blur";
import { useAppHeights } from "./useAppHeights"; // just static constants
import { useScrollPositionForRoute } from "./useScrollPositionForRoute";
import { Platform, StyleSheet } from "react-native";
import Animated, { interpolateColor, interpolate, Extrapolation, useAnimatedStyle } from "react-native-reanimated";
export const AppHeaderBackground = ({ blur }: { blur?: boolean }) => {
const header = useAppHeights();
const scrollY = useScrollPositionForRoute();
const allowBlur = blur && Platform.OS === "ios";
const animatedBackground = useAnimatedStyle(() => {
if (allowBlur) {
return { backgroundColor: "transparent" };
}
const backgroundColor = interpolateColor(scrollY.value, [0, 100], ["rgba(30, 31, 46, 0)", "rgba(30, 31, 46, 1)"]);
return { backgroundColor };
});
const opacityAnim = useAnimatedStyle(() => {
if (!allowBlur) {
return { opacity: 1 };
}
const opacity = interpolate(scrollY.value, [0, 100], [0, 1], Extrapolation.CLAMP);
return { opacity };
});
if (allowBlur) {
return (
<Animated.View style={[styles.headerBackground, { height: header.totalHeight }, opacityAnim]}>
<BlurView style={[styles.blurView, { height: header.totalHeight }]} blurType="dark" blurAmount={95} />
</Animated.View>
);
}
return <Animated.View style={[styles.headerBackground, { height: header.totalHeight }, animatedBackground]} pointerEvents="none" />;
};
const styles = StyleSheet.create({
blurView: {
position: "relative",
zIndex: 0, // Behind header elements
},
absoluteFill: {
// ...StyleSheet.absoluteFillObject,
position: "absolute",
width: "100%",
top: 0,
},
headerBackground: {
position: "absolute",
top: 0,
width: "100%",
zIndex: 0,
},
});
@genesiscz Thanks a lot for your efforts on this, I understand that upgrading React Native is not the most pleasant experience. I'm afraid we need to stick with RN 0.80 as this version comes with various fixes and we need to make sure we are compatible with 0.80 first rather than 0.77. Once we have a fully working solution we'll provide some instructions on how to fix the problem on 0.79, 0.78 and 0.77 hopefully.
we are heavily dependent on react-native-ui-lib which hangs on reanimated 3.16.7 for now causing the conflict
Do you mean that there's some problem with using react-native-ui-lib on the New Architecture?
@tomekzaw sorry forgot to answer...
@genesiscz Thanks a lot for your efforts on this, I understand that upgrading React Native is not the most pleasant experience. I'm afraid we need to stick with RN 0.80
I totally understand and I do not expect you to solve this for us right now, I just wanted to give you some useful data so you are able to get it working for everyone. We're having a big challenge in front of us of migrating 12k files monorepo with web app & RN app to React 19 along with 5 other microfrontend repos so we're definitely having some other trouble in our path to 0.80 đ
Thanks a lot for your efforts!
react-native-ui-lib isn't officially ready for the new architecture yet by their words so I had to override some of their components to make the TabController with PageCarousel work. One nasty bug was https://github.com/wix/react-native-ui-lib/issues/3708 but that wasn't a react-native-ui-lib's bug but rather RNR's (or maybe a RN? If you say it's RN's fault I am going to fill an upstream issue there, too), which I also reported - https://github.com/software-mansion/react-native-reanimated/issues/7453
RNR 4.0.0 broke the TabController pages again - I guess because of some timing issues or maybe some possible reads from .value during render (unfortunately RNUI still has quite a lot of them. I tried to eliminate all possible .value reads in our codebase, but unfortunately without a way to get a stack trace that is not being cut off by Chrome after 10 entries which is full of node_modules is quite hard) - I would have to dig deeper into it to find what the update loop is caused by.
If you are really interested in the react-native-ui-lib bugs we encountered between RN 0.72 -> 0.77 & RNR 3.5.x -> 3.17.5 + turning on new arch (but I am not sure it brings any more value to you):
- 1a) ScrollView's contentOffset was resetting even though it's documented it should be "an initial value" - which in turn re-run onScroll/onMomentum incorrectly
- 1b) useAnimatedScrollHandler's onMomentumEnd was firing at different times and more than before (even when programmatically scrolled - maybe due to the bug I mentioned earlier) - This file had the most problems
- 2a) shared value was setting
nullinstead of the value I changed it to inonLayout(that was on RNR 3.16.7) (logged value just beforesharedValue.value = ...was correct but useAnimatedReaction had the currentValue ofnull) - If you are interested I can give you a bug report I had ready but scratched that because that was on 3.16.7 and I knew you would like me to try it on newest first
Hey, just want to flag that we're also running into this in the Bluesky app. Currently attempting some of these suggested patches https://github.com/bluesky-social/social-app/issues/8535
@mozzius Thanks for letting us know! This issue should be fixed with https://github.com/software-mansion/react-native-reanimated/pull/7668 which has already been merged (note that it requires RN 0.80). So you probably might wanna try upgrading to [email protected] and [email protected].
Thanks @tomekzaw. I gave it a try here, and while it's much better, it's still a little jittery unfortunately - better than before, but still not as good as on paper.
Before
https://github.com/user-attachments/assets/4cf5ddbb-ae7e-444e-a94e-5d6f1d371f07
After
https://github.com/user-attachments/assets/65201ba5-cd62-42be-96a5-b57a9696bced
@mozzius Thanks for giving it a try. #7668 was supposed to fix animations during state update commits (which are triggered as the scroll offset changes) but unfortunately (and also, by design) it doesn't cover the case when commits come directly from React.js which is most likely the case here.
We managed to reproduce a similar problem in our fabric-example app (by placing <ChessboardExample /> component in StickyHeader example) and it went back to normal once we removed isPaused_ = true; line here: https://github.com/software-mansion/react-native-reanimated/blob/6e3796b7c59fb836b7f7c88f9302844fb0eb1f5c/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpp#L21-L23
However, removing isPaused_ = true; can lead to starvation as described in https://github.com/facebook/react-native/issues/51870. That's why we also suggested a new implementation of ShadowTree::commit which is supposed to prevent starvation. You might wanna give it a try (I've tried to build the Bluesky app but got some build errors related to Expo imports in build.gradle).
Hi all, if you're experiencing performance regressions of animations after enabling the New Architecture, please make sure to check out this page we recently added in Reanimated docs:
In the context of this issue, I believe these two links should be particularly useful:
Thanks for sharing this! @tomekzaw In my case, following the docs solved the performance issues. However, enabling the feature flags introduced new problems â for example, some unexpected behavior in FlashList and a few animations acting strangely. Because of that, we had to revert the workaround for our project :(
I believe this might be related to the fact that these feature flags are still experimental and could cause instability. Do you know if this functionality will become the default in the future?
@jesus-lopez-complexity Thanks a lot for your comment!
In my case, following the docs solved the performance issues. However, enabling the feature flags introduced new problems â for example, some unexpected behavior in FlashList and a few animations acting strangely.
Could you please specify which feature flags exactly you enabled and what the "unexpected behavior in FlashList" was how exactly "a few animations acting strangely" looked like? Unfortunately this kind feedback is too generic for us to be able to do anything about it. If possible, please share video recording of broken components or screens, preferably with code snippets.
I believe this might be related to the fact that these feature flags are still experimental and could cause instability. Do you know if this functionality will become the default in the future?
Yes, they are considered experimental but we're thinking about enabling these feature flags in a future release so it's quite important for us to have them working correctly.