Skip to content

Speed up the side menu and fix its overlay fade on Android - #6298

Draft
peachbits wants to merge 8 commits into
developfrom
matthew/android-drawer-perf
Draft

peachbits wants to merge 8 commits into
developfrom
matthew/android-drawer-perf

Conversation

@peachbits

@peachbits peachbits commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

Asana: Android sidebar blur doesn't fade in and out

On a large account on Android, the side menu was slow to start opening, its dimming overlay didn't fade, and hiding or showing balances was slow to flip. I profiled release builds on an API 35 emulator with a large account, using temporary trace logging and frame-by-frame screen recordings (not part of this PR), and reworked this PR around what they showed:

  • Stop re-dispatching the info refresh on every Services render. This is the main cause of the slow open. Services passes useRefresher an inline callback, and the hook restarts and fires immediately whenever its callback changes. The side menu hosts Services, so every open and close re-ran updateExchangeInfo, updateGiftCardInfo and updateHoudiniChains, each dispatching a fresh object to every subscriber. React ran them before the drawer's own effect could start the animation. useHandler keeps the callback stable.
  • Memoize Services. This also skips re-rendering every service, including the wallet sort, on each toggle.
  • Fade the overlay without reordering views. The drawer overlay animated zIndex from -1 to 0 as it opened. Under the new architecture that reorders the native views and draws a fully blank frame of the app. The existing @react-navigation/drawer patch now leaves the overlay in front, and its animated pointerEvents keep it from blocking touches while closed.
  • Flip balance visibility before saving it (unchanged from the first version).

Dropped from the first version:

  • The EdgeAppStack memo: the stack never re-rendered on a drawer toggle.
  • ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS: it applies the overlay's opacity directly, but the zIndex change still waits for a commit, so the overlay faded behind the screen and popped in after the drawer had opened, on 5 of 8 opens even on a quiet device. Without the zIndex animation, the fade is the same with or without the flag.

Results (API 35 emulator, large account, release builds):

Before After
Drawer start after the press, quiet 86 ms 0 ms
Drawer start after the press, under sync load 1.1-5.8 s 3-5 ms
Overlay on open, quiet 1 blank frame, then a 4-11 frame fade 10-12 frame fade, no blank frame (8 of 8 opens)
Balance tap to UI flip, under sync load 8-17 s 1-2.4 s

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

@peachbits
peachbits force-pushed the matthew/android-drawer-perf branch from 021209a to 406b25e Compare October 29, 2026 20:19
@peachbits peachbits changed the title Speed up the side menu and balance toggle on large accounts Speed up the side menu and fix its overlay fade on Android Oct 29, 2026
peachbits and others added 7 commits October 29, 2026 13:50
Patch releases on the 0.10 line, which Reanimated 4.5 pairs with. On
Android the UI runtime's frame loop kept running while the app was in
the background (0.10.4), and worklet functions could be destroyed after
their runtime was gone, crashing on teardown (0.10.1). The other fixes
are in the Babel plugin, Android bundle mode, and iOS background worklet
runtimes, which the app does not use.
Patch releases on the 4.5 line. 4.5.4 fixes a crash when an event
arrives for a view that has already unmounted, an iOS launch crash when
the UI queue flushes before Reanimated has registered with it, and an
Android props merge that let the oldest of several updates to one view
in a commit win, so scroll-driven animations could jump back a frame. It
also stops taking over React Native's LayoutAnimation driver; the only
LayoutAnimation caller in the app's dependencies is gifted-charts'
BarChart, which the app does not use. 4.5.5 only fixes upstream CI.

Neither release has the layout animation final-frame fix, which the next
commit patches in.
Reanimated 4.5's legacy layout animation proxy, which the app uses,
drops an animation's last pending frame once the animation settles. The
final progress update and the end callback arrive in the same frame, and
the flush that follows skips settled animations, so views stop one frame
short of their target. On a busy device the first frame can already be
the last one, and the view stays where it started. On Home, the rows
below the promo carousel stayed where they were before the carousel
appeared, so the carousel drew over the Buy and Sell tiles.

Patch in the upstream fix (Reanimated PR 10171, released in 4.6.0): skip
a settled animation's pending update only when its view is exiting. The
experimental proxy, used only with the shared element transitions flag,
is left alone.

On an Android emulator with a large account and the carousel forced in
4 s late, the Buy tile stopped 1 px short of its target on 4 of 4
launches before and landed on it on 6 of 6 after. Across cold launches
the carousel covered Buy and Sell on 3 of 10 before, and on none of 8
under in-emulator CPU load after.
The side menu hosts Services and re-renders whenever the drawer opens
or closes, which re-ran every service, including the SortedWalletList
sort, before the drawer could start moving. Memoize Services so a drawer
toggle only re-renders the menu itself.
useRefresher restarts its timer, and calls its callback immediately,
whenever the callback changes. Services passed an inline function, so
every Services render re-ran updateExchangeInfo, updateGiftCardInfo and
updateHoudiniChains, each dispatching a fresh object to every subscriber.
Services re-renders on its own selectors too, not only when the drawer
toggles, so the memo alone cannot cover this.

On a large account on an Android emulator, the two slowest dispatches
took 0.5-1.1 s each per drawer toggle, all before the drawer's effect
could start the open animation.
The toggle waited on the core's settings write before updating the UI,
which can take seconds while a large account syncs. Update redux right
away and roll back if the write fails.
The drawer overlay animated zIndex from -1 to 0 as the drawer started
to open, sending it from behind the screen to in front of it. Under the
new architecture that reorders the native views, which drew a fully
blank frame of the app on most opens. The overlay's animated
pointerEvents already keep the closed overlay from taking touches, so
leave it in front and let opacity alone fade it.

On an Android emulator with a large account, opens went from a blank
frame plus a 4-11 frame fade to a 10-12 frame fade with no blank frame.

Upstream removed the same zIndex for a white flicker under the new
architecture (react-native-drawer-layout 4.0.4, commit f662e274). That
fix only reached the v7 drawer; 6.7.2 is the last 6.x release.
@socket-security

socket-security Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedreact-native-worklets@​0.10.0 ⏵ 0.10.4991008598100
Updatedreact-native-reanimated@​4.5.3 ⏵ 4.5.51001009098100

View full report

@react-navigation/drawer 6.7.2 wraps the app in a View whose
accessibilityElementsHidden and importantForAccessibility props flip
with the drawer's open state. Under the new architecture a View that
gains those props stops being flattened, and one that loses them is
flattened away, so every open and close moved the whole app to a new
native parent. A Pixel 9 Pro drew a frame of that move with the app
missing: the screen outside the menu went flat dark as the menu opened
or closed.

Mark the wrapper collapsable={false} in the existing drawer patch so it
is always a real native view. The v7 drawer avoids this by using an
Animated.View, which Reanimated never flattens.

On the Pixel, with the menu opened and closed 12 times on the Assets
tab, the empty frame showed up on 12 of 12 cycles before and on none
after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant