react-native-safe-area-context icon indicating copy to clipboard operation
react-native-safe-area-context copied to clipboard

Performance issue with SafeAreaView

Open archansel opened this issue 4 years ago • 17 comments

I notice some performance issue while using SafeAreaView. These performance issue will not happen in high end device (because it render realy fast) bug

Currently I endup using useSafeAreaInsets because it perform way better (it render correct insets on first render). Did I do something wrong? or maybe it is a bug?

"@react-navigation/native": "5.6.0" "react-native-safe-area-context": "3.0.7"

archansel avatar Jul 01 '20 20:07 archansel

@archansel Faced with same issue, did you fix this problem?

avet-m avatar Jul 08 '20 07:07 avet-m

Nope, as I said, I end up using useSafeAreaInsets and avoiding SafeAreaView as much as I can because of this performance issue

archansel avatar Jul 08 '20 09:07 archansel

@archansel thank you, yes also use useSafeAreaInsets, but it triggers multiple times

LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 44}}
 LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 44}}
 LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 0}}
 LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 0}}
 LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 44}}
 LOG  {"insets": {"bottom": 0, "left": 0, "right": 0, "top": 44}}

as you can see in the logs, there is lots of flickers in top property, as described here

avet-m avatar Jul 08 '20 11:07 avet-m

luckily for me, my current implementation doesn't give me any flickers, and when I check using logs, insets stay the same all the time (my app only use portrait orientation)

another note, SafeAreaView problem only happens if I render 'heavy' list item, for basic list item it seems fine

archansel avatar Jul 08 '20 11:07 archansel

@archansel yes, problem occurred when use both portrait and landscape orientation

avet-m avatar Jul 08 '20 11:07 avet-m

For me, this issue only occurs when the SafeAreaView is first rendered while outside the viewport. Here's what I think is going on.

In order to decide whether to offset its children, SafeAreaView needs to know where it is being laid out on-screen. For instance, if you have a navigation layout where you have a tab bar at the bottom, and a SafeAreaView wrapping the main content above the TabBar, the SafeAreaView should avoid applying any bottom padding.

When SafeAreaView's initial render is outside the viewport, it doesn't know whether it should apply padding. Using useSafeAreaInsets goes around this issue because you are explicitly deciding which padding to apply. Another workaround is to use react-native-safe-area-view, which has a forceInset prop that lets you explicitly decide which insets to apply.

Ashoat avatar Jul 26 '20 02:07 Ashoat

@archansel Thanks for your solution, it is working perfectly even on very old devices both iOS and Android.

Here is the SafeAreaView I created:

type SafeAreaViewProps = {
  disableBottomSafeArea?: boolean,
  disableTopSafeArea?: boolean,
  disableSidesSafeArea?: boolean,
  children: React.Node
}

export const SafeAreaView: FunctionComponent<SafeAreaViewProps> = (props: SafeAreaViewProps) => {

  const { disableBottomSafeArea = false, disableTopSafeArea = false, disableSidesSafeArea = false, children } = props;

  const insets = useSafeAreaInsets();

  const style: StyleSheet = {};

  if (!disableBottomSafeArea) {
    style.marginBottom = insets.bottom;
  }

  if (!disableTopSafeArea) {
    style.marginTop = insets.top;
  }

  if (!disableSidesSafeArea) {
    style.marginRight = insets.right;
    style.marginLeft = insets.left;
  }

  return <View style={[{ flex: 1 }, style]}>
    {children}
  </View>;
};

Rotemy avatar Aug 01 '20 11:08 Rotemy

@Rotemy and @archansel Thank you, thats a big help!

Reizar avatar Oct 07 '20 04:10 Reizar

Thank you! @archansel @Rotemy

MauriceArikoglu avatar May 17 '21 12:05 MauriceArikoglu

What's up with that custom SafeAreaView ? It looks like you coded the SafeAreaView again The original SafeAreaView also uses the hook useSafeAreaInsets, and applies margin (or padding) on the edges you want

Snowirbix avatar Jul 13 '21 09:07 Snowirbix

Brain dump of some investigation I did into this:

I'm seeing something similar, and it doesn't have to be rendered outside the viewport. On iOS, the SafeAreaView shows up without any padding when it first enters the hierarchy, and then the padding is added. Most of the time it's too fast to show up in a frame, but sometimes it does, particularly when there's "a lot of other stuff going on", so I would expect it to be more common on older devices as well. Regardless, the layout with zero padding always happens (whether you see it or not). Using initial window metrics in the provider does not solve this.

This appears to be a bug in the iOS implementation of the native version of SafeAreaView. I'm a little out of my depth, but I see we're invalidating the insets in several lifecycle-y methods: safeAreaInsetsDidChange, layoutSubviews, and didMoveToWindow. It looks like didMoveToWindow is at least one culprit wherein the native frameworks return empty insets (all 0) when the view is first added to the hierarchy. I question whether we need to be setting insets from all three of these places (though I have no idea), and safeAreaInsetsDidChange (which I have a suspicion is the "most correct" one) is actually returning early and not invaliding the insets due to the fact that the _providerView (which I'm guessing has something to do with the SafeAreaViewProvider) is null at the time. I haven't chased down when/how the _providerView gets set, or whether its actually the source of the insets we're getting. That's where I stopped.

As for temporary solutions, neither useSafeAreaInsets nor SafeAreaViewContext.Consumer seem to exhibit this same behavior, which is why @Rotemy's code above works. However, I agree with @Snowirbix that it's a reimplementation of the non-native version of SafeAreaView, and is incomplete in that it just wraps the children, rather than modifying any passed-in styles. (Also the default behavior would be to modify the padding, not the margin.)

I haven't found a way to ignore the platform-specific (native) version and specifically import the non-native version, but I'm not sure how safe that would be to do anyway since in theory that version would be meant for react-native-web and could therefore break down the road, so my solution has been to copy the non-native SafeAreaView implementation and modify it for our purposes, since it's effectively just an example of how to use useSafeAreaInsets. (I also changed it to be class-based and use SafeAreaViewContext.Consumer instead.)

TLDR: The native (iOS) implementation of SafeAreaView briefly has zero padding/margin when it first enters the layout. The non-native (pure TypeScript) version does not.

jonthanon avatar Jul 22 '21 23:07 jonthanon

Interesting, @jonthanon, I am actually seeing something similar what you described in Android only and not in iOS. On my Android device, inset.bottom renders first at 16 and then updates to the correct value, 0.

taylorkline avatar Aug 20 '21 15:08 taylorkline

Brain dump of some investigation I did into this:

Thanks for the dump @jonthanon. I've dug a bit deeper and placed some logs into RNCSafeAreaView to figure out what's causing the issues. This is what gets logged when navigating to a screen in which I have a SafeAreaView wrapping a custom Header component.

2021-11-06 14:16:51.915841+0100 RNCSafeAreaView: didMoveToWindow
2021-11-06 14:16:51.919771+0100 RNCSafeAreaView: safeAreaInsetsDidChange
2021-11-06 14:16:51.919855+0100 RNCSafeAreaView: invalidateSafeAreaInsets before {0.000000, 0.000000, 0.000000, 0.000000}
// flicker!
2021-11-06 14:16:51.919875+0100 RNCSafeAreaView: invalidateSafeAreaInsets after {0.000000, 0.000000, 0.000000, 48.000000}
2021-11-06 14:16:51.919927+0100 RNCSafeAreaView: safeAreaInsetsDidChange
2021-11-06 14:16:51.926069+0100 RNCSafeAreaView: layoutSubviews
2021-11-06 14:16:51.947294+0100 RNCSafeAreaView: safeAreaInsetsDidChange
2021-11-06 14:16:51.947397+0100 RNCSafeAreaView: safeAreaInsetsDidChange
2021-11-06 14:16:51.948794+0100 RNCSafeAreaView: layoutSubviews

I found that the reason for this flicker is because the _currentSafeAreaInsets is initialized with default values of 0. We then need to wait for safeAreaInsetsDidChange to set the correct values. Setting default values for _currentSafeAreaInsets doesn't help either, because they are reset to zeros by the didMoveToWindow method. That is what I found from the logs, please correct me if I'm wrong.

TLDR: The native (iOS) implementation of SafeAreaView briefly has zero padding/margin when it first enters the layout. The non-native (pure TypeScript) version does not.

Unfortunately, in my case the non-native version does not suffice. During the first render, useSafeAreaInsets returns insets relative to the entire viewport. I am only trying to wrap the header, so I don't need the bottom inset. This also lead to a flicker:

 LOG  INSETS {"bottom": 34, "left": 0, "right": 0, "top": 48}
 LOG  INSETS {"bottom": 0, "left": 0, "right": 0, "top": 48}

I know that I am free to use only the "top" value, in which case there is no flicker with this method, but I have another use-case. I would like to have paddingBottom = Math.max(insets.bottom, 20) for some footer that does not overlap the home indicator. In this case there will be a flicker as the padding changes from 34 to 20.

I'm not sure how to solve this and whether it can even be solved. Can any of the maintainers please take a look? The utility that this SafeAreaView provides is completely clouded by this annoying flicker. Usually it happens quickly, but as some have noted earlier, it causes problems on older devices and heavy renders.

aamikus avatar Nov 06 '21 15:11 aamikus

Can confirm I am seeing this too with the native SafeAreaView. It's weird that it happens intermittently

Initial load:

then insets applied:

which causes a flicker:

https://user-images.githubusercontent.com/984574/145834742-f1994d48-d158-48ae-9d17-4a21d4e07c9c.mov

stopachka avatar Dec 13 '21 14:12 stopachka

Seeing the same thing. This happens with RN's own SafeAreaView, and I was hoping that this library wouldn't have the same issue, but it does!

cristianoccazinsp avatar Apr 06 '22 14:04 cristianoccazinsp

FWIW, I've been using the same workaround as @Rotemy.

stopachka avatar Apr 06 '22 15:04 stopachka

Interesting, @jonthanon, I am actually seeing something similar what you described in Android only and not in iOS. On my Android device, inset.bottom renders first at 16 and then updates to the correct value, 0.

I have the exact same problem on my android device. When I navigate from Screen B to Screen A, the Screen A bottom inset is 0 then 16 then 0 in a fraction of time, causing the flicker.

The react-native-safe-area-context doc says "if [the SafeAreaProvider] overlaps with any system elements (status bar, notches, etc.) these values will be provided to descendent consumers".

I don't know where this 16 value comes from as it's still happening even when I disable the screen transition animations.

The only workaround I found is to use a custom tabBar. https://reactnavigation.org/docs/bottom-tab-navigator/#tabbar The custom tabBar function actually gets those props: { state, descriptors, navigation, insets } So I created my own TabBar component based on the example provided in the docs and didn't added the insets values.

I guess the default tabBar has it's marginBottom changed by insets.bottom.

samvoults avatar Jul 26 '22 10:07 samvoults

This is for v3 of the library, and we're on v4. Please re-open a new issue if you're still facing issues

jacobp100 avatar Jan 19 '23 16:01 jacobp100

This still happening with v4.5 on react navigation animation transition.

hadnet avatar Feb 23 '23 04:02 hadnet

The hook useSafeArea has a delay compared to the native SafeAreaView, which causes a flicker. You can mitigate some of it with initialMetrics - but if you rotate to landscape or something, you'll see a flicker again - and we can't fix it at the moment. If you - or any libraries you are using - are using the hook, you will see a flicker. The real fix is to make sure every single instance is using the native view.

jacobp100 avatar Feb 23 '23 09:02 jacobp100