Skip to content

Shadows are causing fps drops during animations on IOS #49128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MaksymAleieiv opened this issue Feb 3, 2025 · 12 comments
Closed

Shadows are causing fps drops during animations on IOS #49128

MaksymAleieiv opened this issue Feb 3, 2025 · 12 comments
Assignees
Labels
Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Platform: iOS iOS applications. Resolution: PR Submitted A pull request with a fix has been provided.

Comments

@MaksymAleieiv
Copy link

MaksymAleieiv commented Feb 3, 2025

Description

Shadows are causing fps drops during animations on IOS. Reproduction uses Expo for ease of use, this bug happens in bare projects without expo. The Reproduction uses react-native 0.76.6, but the problem exists with react-native 0.77 too

Steps to reproduce

  1. Open Snack provided in Reproduction
  2. Use "My device" option
  3. Try to change isFullShadow and hasBorderRadius and run animation to see fps drop

React Native Version

0.76.6

Affected Platforms

Runtime - iOS

Output of npx react-native info

System:
  OS: macOS 14.5
  CPU: (8) arm64 Apple M2
  Memory: 169.80 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.6.0
    path: /opt/homebrew/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.8.2
    path: /opt/homebrew/bin/npm
  Watchman:
    version: 2024.08.19.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.14.3
    path: /Users/maksymaleieiv/.rvm/gems/ruby-3.3.1/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK:
    API Levels:
      - "33"
      - "34"
      - "35"
    Build Tools:
      - 34.0.0
      - 35.0.0
    System Images:
      - android-34 | Google APIs ARM 64 v8a
      - android-34 | Google Play ARM 64 v8a
      - android-35 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2024.2 AI-242.23339.11.2421.12550806
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.11
    path: /usr/bin/javac
  Ruby:
    version: 3.3.1
    path: /Users/maksymaleieiv/.rvm/rubies/ruby-3.3.1/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 15.1.3
    wanted: latest
  react:
    installed: 18.3.1
    wanted: 18.3.1
  react-native:
    installed: 0.76.6
    wanted: 0.76.6
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Stacktrace or Logs

No Stacktrace

Reproducer

https://snack.expo.dev/@maksym4062/shadow-animation-freeze-mre

Screenshots and Videos

Full shadow without radius => 60fps
https://github.com/user-attachments/assets/750c7b08-5881-432d-b82a-bef62e2ac28f
Alpha shadow without radius => 40-50fps
https://github.com/user-attachments/assets/bb763084-70d9-4d4a-ad23-70a7b79a6571
Full shadow with radius => 30-40fps
https://github.com/user-attachments/assets/3ff472cd-adeb-41a3-8115-3640cb74ef7e
Alpha shadow with radius => 20-30fps
https://github.com/user-attachments/assets/386f55dd-92c2-4628-9fde-2d876ab0e2cf

@migueldaipre
Copy link
Collaborator

I can reproduce on my side. Physical iPhone 12

@migueldaipre migueldaipre added Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. and removed Needs: Triage 🔍 labels Feb 3, 2025
@NickGerleman
Copy link
Contributor

FYI @joevilches for when back from PTO who worked on iOS impl of box shadow

In the past we talked about using CALayer shadows when we had a happy path to do so. But there might also just be extra work we are doing on redraw that we shouldn't be.

@joevilches
Copy link
Contributor

Acknowledging I saw this, thanks for the report! There is another iOS box shadow issue reported at #49134 which I will tackle before this one.

@MaksymA26
Copy link

Hi, @joevilches do you have any updates on this issue?

Our iOS app have been stack on old shadows api for a month now, would really appreciate if you could fix this issue 🙏
Or at least tell us when or if we can expect the fix. Thanks

@AndrewPrifer
Copy link

Just ran into this too. Please let me know if there's anything I can do to help get this through the finish line. :)

@joevilches
Copy link
Contributor

Hey thanks for the check ins! Yeah so update here is that I took a look at the issue and can confirm I am seeing it too (ty for providing repro). With the specific animation being used (changing height/width of shadow), there will have to be redrawing, which is unfortunate and slow. That being said I think there may be a path forward which would involve changing how RN does its layer drawing to hook into https://developer.apple.com/documentation/quartzcore/calayer/display()?language=objc on a specific layer (i.e. utilize UIKit's drawing lifecycle which might provide some optimizations). I think this would be quite challenging as some of RCTViewComponentView will need to change to get this to work which might lead to having to migrate other layers to use this as well (borders, background color, filter, etc). I am currently working on improving some accessibility APIs we have, so cannot prioritize this at the moment. I have some spare time in early April I can try to tackle this then!

Also, we could implement a fast-case were we use iOS shadow APIs in the cases where we can use them (not inset, no spread), but it seems that might not suffice given you mentioned you are using the old shadows currently.

@AndrewPrifer
Copy link

@joevilches, thanks for the detailed explanation! In my situation, I notice a performance drop even when I'm not directly animating the element with the shadow. The shadowed element stays the same, I only animate its child elements. Despite this, it seems like the shadow is still being redrawn unnecessarily. Would it be as difficult to eliminate these unnecessary redraws as it is to optimize the animation of the element with the shadow?

@AndrewPrifer
Copy link

@joevilches A workaround I found for my situation (where the shadow-casting container is not animated, only its children), is to create a dedicated shadow caster inside the container as a sibling to the animated children, in which case performance is unaffected. I think as a low hanging fruit, it would be nice to have a builtin optimization at least for this case (which I imagine to be a common case).

Before (bad):

<View style={{ boxShadow: '...' }}>
  <Animated.View>...</Animated.View>
</View>

After (good):

<View>
  <View style={{ ...StyleSheet.absoluteFillObject, boxShadow: '...' }}>
  <Animated.View>...</Animated.View>
</View>

@joevilches
Copy link
Contributor

joevilches commented Mar 12, 2025

@AndrewPrifer thanks for the info this is interesting and will help a ton when I go to tackle this. Glad you were able to find a workaround but I agree this case should be able to work performantly with your before code

joevilches added a commit to joevilches/react-native that referenced this issue Apr 10, 2025
Summary:
A while ago someone on GitHub reported that box shadow on iOS was causing frame drops when animating a large, pretty blurry shadow: facebook#49128. This week I finally got around to fixing this!

The slowness was happening since we were using CG to draw this shadow, which is very CPU intensive and, to my knowledge, does not take advantage of GPUs to do anything. Couple that with an animating, large, blurry shadow and we have frame issues. These shadows were taking very long to draw, to get the image of the shadow (which then needs to be copied into some texture 3 times as big, composited, put on the screen etc) took 12-14ms :o, thats very slow.

To fix this I figured out how to get CA's shadow APIs working, which take advantage of the GPU. The enable inset shadows and spread you have to get creative with a mix of `shadowPath` and `mask` with a `CAShapeLayer`, but we got it done! Things are much faster, I am not sure how to time this but using a real device shows no frame drops :D

Changelog: [iOS][Fixed] - Box shadows on iOS are faster

Differential Revision: D72823334
@joevilches
Copy link
Contributor

It's early April and that means we are making shadows faster! I don't wanna be too hasty since my PR still needs review from a peer, but things are much faster with this new implementation!

before:
https://github.com/user-attachments/assets/7d768388-f945-441c-9540-8695c9d9d827

after:
https://github.com/user-attachments/assets/1c2d2e60-f4c3-4789-aa2a-d6a01bfd6622

The implementation I went with was different than the one I guessed above. I tried that and while it did improve the speed quite a bit, there was still very noticeable frame drops. The implementation I landed on uses CA layer shadow APIs, which are the same ones used by these guys: https://reactnative.dev/docs/shadow-props#shadowcolor. TLDR, its probably the fastest we can get this since we are using Apple's shadow APIs now and not doing our own custom drawing :D

Hopefully there is no glaring hole in this implementation but I tested quite a few cases and it all seems to be the same, with some small visual clarity wins compared to the old impl. Hope this helps!

@migueldaipre migueldaipre added the Resolution: PR Submitted A pull request with a fix has been provided. label Apr 10, 2025
joevilches added a commit to joevilches/react-native that referenced this issue Apr 10, 2025
Summary:

A while ago someone on GitHub reported that box shadow on iOS was causing frame drops when animating a large, pretty blurry shadow: facebook#49128. This week I finally got around to fixing this!

The slowness was happening since we were using CG to draw this shadow, which is very CPU intensive and, to my knowledge, does not take advantage of GPUs to do anything. Couple that with an animating, large, blurry shadow and we have frame issues. These shadows were taking very long to draw, to get the image of the shadow (which then needs to be copied into some texture 3 times as big, composited, put on the screen etc) took 12-14ms :o, thats very slow.

To fix this I figured out how to get CA's shadow APIs working, which take advantage of the GPU. The enable inset shadows and spread you have to get creative with a mix of `shadowPath` and `mask` with a `CAShapeLayer`, but we got it done! Things are much faster, I am not sure how to time this but using a real device shows no frame drops :D

Changelog: [iOS][Fixed] - Box shadows on iOS are faster

Reviewed By: lenaic

Differential Revision: D72823334
joevilches added a commit to joevilches/react-native that referenced this issue Apr 10, 2025
Summary:
Pull Request resolved: facebook#50636

A while ago someone on GitHub reported that box shadow on iOS was causing frame drops when animating a large, pretty blurry shadow: facebook#49128. This week I finally got around to fixing this!

The slowness was happening since we were using CG to draw this shadow, which is very CPU intensive and, to my knowledge, does not take advantage of GPUs to do anything. Couple that with an animating, large, blurry shadow and we have frame issues. These shadows were taking very long to draw, to get the image of the shadow (which then needs to be copied into some texture 3 times as big, composited, put on the screen etc) took 12-14ms :o, thats very slow.

To fix this I figured out how to get CA's shadow APIs working, which take advantage of the GPU. The enable inset shadows and spread you have to get creative with a mix of `shadowPath` and `mask` with a `CAShapeLayer`, but we got it done! Things are much faster, I am not sure how to time this but using a real device shows no frame drops :D

Changelog: [iOS][Fixed] - Box shadows on iOS are faster

Reviewed By: lenaic

Differential Revision: D72823334
@AndrewPrifer
Copy link

@joevilches awesome, thank you so much for your work!! 🚀🚀

Just to make sure, since you said you changed from custom drawing to CA layer shadows, does it still conform to the box shadow spec? E.g. does it handle transparency the same way (not showing through, and view transparency doesn't determine shadow strength)?

@joevilches
Copy link
Contributor

@AndrewPrifer I tested it on a large range of values, and it seems to conform as much as the old implementation did

not showing through

What do you mean by this?

View transparency doesn't determine shadow strength

If the View does not have a backgroundColor it will still show the box shadow. CALayer's shadowPath will cast shadows all along the path regardless of what color the layer is at that point. If you set opacity on the View with the boxShadow, however, the shadow will reflect that opacity, but that is how web works.

facebook-github-bot pushed a commit that referenced this issue Apr 11, 2025
Summary:
Pull Request resolved: #50636

A while ago someone on GitHub reported that box shadow on iOS was causing frame drops when animating a large, pretty blurry shadow: #49128. This week I finally got around to fixing this!

The slowness was happening since we were using CG to draw this shadow, which is very CPU intensive and, to my knowledge, does not take advantage of GPUs to do anything. Couple that with an animating, large, blurry shadow and we have frame issues. These shadows were taking very long to draw, to get the image of the shadow (which then needs to be copied into some texture 3 times as big, composited, put on the screen etc) took 12-14ms :o, thats very slow.

To fix this I figured out how to get CA's shadow APIs working, which take advantage of the GPU. The enable inset shadows and spread you have to get creative with a mix of `shadowPath` and `mask` with a `CAShapeLayer`, but we got it done! Things are much faster, I am not sure how to time this but using a real device shows no frame drops :D

Changelog: [iOS][Fixed] - Box shadows on iOS are faster

Reviewed By: lenaic

Differential Revision: D72823334

fbshipit-source-id: 460339c9d77e7423ce59a1a9178b6b3ad527e4b0
uffoltzl pushed a commit to uffoltzl/react-native that referenced this issue Apr 18, 2025
Summary:
Pull Request resolved: facebook#50636

A while ago someone on GitHub reported that box shadow on iOS was causing frame drops when animating a large, pretty blurry shadow: facebook#49128. This week I finally got around to fixing this!

The slowness was happening since we were using CG to draw this shadow, which is very CPU intensive and, to my knowledge, does not take advantage of GPUs to do anything. Couple that with an animating, large, blurry shadow and we have frame issues. These shadows were taking very long to draw, to get the image of the shadow (which then needs to be copied into some texture 3 times as big, composited, put on the screen etc) took 12-14ms :o, thats very slow.

To fix this I figured out how to get CA's shadow APIs working, which take advantage of the GPU. The enable inset shadows and spread you have to get creative with a mix of `shadowPath` and `mask` with a `CAShapeLayer`, but we got it done! Things are much faster, I am not sure how to time this but using a real device shows no frame drops :D

Changelog: [iOS][Fixed] - Box shadows on iOS are faster

Reviewed By: lenaic

Differential Revision: D72823334

fbshipit-source-id: 460339c9d77e7423ce59a1a9178b6b3ad527e4b0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Platform: iOS iOS applications. Resolution: PR Submitted A pull request with a fix has been provided.
Projects
None yet
Development

No branches or pull requests

7 participants