perf(mobile): avoid runOnUISync in chart Gradient effect#774
Closed
AndreiCalazans wants to merge 4 commits into
Closed
perf(mobile): avoid runOnUISync in chart Gradient effect#774AndreiCalazans wants to merge 4 commits into
AndreiCalazans wants to merge 4 commits into
Conversation
Production Android CPU profiles of chart-heavy mobile surfaces in
Coinbase Retail showed an anonymous function inside the chart
`Gradient` component accounting for ~1.62s (28%) of CPU time across
65 invocations, mean ~29ms per call. The flame graph stack:
commitHookEffectListMount
└─ anonymous (Gradient.tsx, 'toPositions.value' getter)
└─ get
└─ runOnUISync
└─ runOnUISync
└─ [HostFunction] runOnUISync
In Reanimated 4 (the version cds-mobile currently lists as a peer
dep) shared-value semantics changed: shared values live on the UI
runtime, so a JS-thread read of `sharedValue.value` is no longer
a cheap cached local read - it is a synchronous JS<->UI round-trip
via `runOnUISync`. That round-trip is what shows up in the trace.
The Gradient effect reads `toPositions.value` twice on every run:
1. `toPositions.value.length === targetPositions.length`
2. `fromPositions.value = [...toPositions.value]` in the
can-animate branch
But this component is the *sole writer* of `toPositions.value` -
every effect run writes `[...targetPositions]` - so the JS thread
already knows what was last written and never needs to read it back
over the bridge.
Fix: mirror the array we last wrote into a JS-side useRef
(`lastWrittenPositionsRef`) and read from the ref in both places.
No behavior change:
- The effect still detects stop-count changes.
- It still animates when the count is stable.
- It still snaps when the count changes.
Validated locally with:
yarn nx run mobile:typecheck # green
yarn nx run mobile:lint # green
yarn nx run mobile:test # 170 suites / 1845 tests pass
Collaborator
✅ Heimdall Review Status
✅
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
✅
1/1
|
Denominator calculation
|
…cial bumps) Generated via: yarn bump-version mobile --bump patch --message '...' --pr 774 yarn release # keeps web/common/mcp-server in sync per CONTRIBUTING.md Also retargets the changelog PR link from the fork to coinbase#774.
…cial bumps) Re-generated after merging master (9.5.0) via: yarn bump-version mobile --bump patch --message '...' --pr 774 yarn release # keeps web/common/mcp-server in sync per CONTRIBUTING.md Also retargets the changelog PR link from the fork to coinbase#774.
cb-ekuersch
approved these changes
Jun 26, 2026
6 tasks
Member
Author
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed? Why?
Production Android CPU profiles of chart-heavy mobile surfaces in the Coinbase Retail app showed an anonymous function inside the chart
Gradientcomponent accounting for ~1.62s of CPU time (28%) across 65 invocations, with each slice averaging ~29ms. The flame graph stack:Root cause (required for bugfixes)
In Reanimated 4 (the version
@coinbase/cds-mobilecurrently lists as a peer dep), shared-value semantics changed. Shared values now live on the UI runtime, so a JS-thread read ofsharedValue.valueis no longer a cheap cached local read — it is a synchronous JS↔UI round-trip viarunOnUISync. That round-trip is what shows up in the trace.The Gradient effect reads
toPositions.valuetwice on every run:const canAnimatePositions = toPositions.value.length === targetPositions.length;fromPositions.value = [...toPositions.value];in the can-animate branch.But this component is the sole writer of
toPositions.value— every effect run writes[...targetPositions]— so the JS thread already knows what was last written and never needs to read it back over the bridge.Before
Note anonymous function pointing to line 206 in the Gradient.js file
After
Note anonymous function pointing to line 206 in the Gradient.js file is fully gone in the Sanduiche bottom up view for runOnUISync
Testing
How has it been tested?
yarn nx run mobile:test— 170 suites / 1845 tests pass)Testing instructions
Gradientwith an animated underlying data source whose stop count is sometimes stable and sometimes changing (e.g. a line chart with a gradient stroke whose data refreshes periodically).x/y) render as before.runOnUISynccluster under the Gradient effect that was previously present (~29ms × N invocations) should be essentially gone.Illustrations/Icons Checklist
N/A — does not touch
packages/illustrations/**orpackages/icons/**.Change management
type=routine
risk=low
impact=sev5
automerge=false