Repository navigation
[Android][Fabric] A decoded local image can miss the current frame during a long mount #59158
Description
Description
On Android with Fabric, a local Image that misses Fresco's bitmap memory cache can be drawn with its placeholder even when its decoded result is ready before that frame is drawn. The image is applied one or more frames later.
The reproducer mounts two Image components with the same local source in one commit:
- Image A uses
cache: 'reload', which evicts that URI before requesting it. - A small but nested vote-list subtree is mounted between A and B.
- A is decoded on a background thread while the UI thread is processing the mount.
- Image B is submitted later and synchronously hits the bitmap that A has placed in the memory cache.
The first visible frame can therefore contain A's gray placeholder and B's image, even though both use the same source.
The delivery path for A is:
AbstractDraweeController.submitRequest
→ DataSource.subscribe(subscriber, uiThreadExecutor)
→ a Handler message posted to the UI thread
→ SettableDraweeHierarchy.setImage
When the result becomes ready while the UI thread is already inside a long Choreographer.doFrame, the posted message cannot run between Fabric's mount work and traversal/draw because they execute inside the same frame message. The frame is drawn with the placeholder. If another overdue frame starts before the posted result message, the delay is amplified by that frame as well.
sequenceDiagram
participant UI as UI thread
participant BG as Fresco background thread
UI->>BG: Submit A, bitmap memory-cache miss
UI->>UI: Continue Fabric mount work
BG-->>UI: Decode A, post result Handler message
UI->>UI: Submit B, bitmap memory-cache hit
UI->>UI: Draw frame with A placeholder and B image
UI->>UI: Run A result message after the frame
UI->>UI: Draw A image in a later frame
This is intermittent in applications because a bitmap memory-cache hit is delivered synchronously and avoids the problem. A miss occurs on first use, after eviction or cache trimming, and when an app clears Fresco's memory cache. The reproducer uses cache: 'reload' only to make A start cold on every run.
On React Native 0.88.0, screen recording captured the A-placeholder/B-image frame in 10 of 10 runs with the final reproducer on a Vivo V2338A running Android 16 (API 36). The measured B-to-A display delays were:
78, 67, 67, 62, 10, 75, 77, 74, 68, 75 ms
Median: approximately 71 ms. Nine of the ten runs lasted 62–78 ms.
In a production app, the same pattern occurs with a local file:// button image during the first commit of a screen and remains visible for approximately 19–60 ms.
Expected behavior: if an image result is ready before a frame is drawn, that frame should draw the image, matching the behavior of a synchronous bitmap memory-cache hit.
Steps to reproduce
-
Check out React Native
0.88-stableat99d4ed39851392b4c65fd38fe3812669cb493376. -
Replace
packages/rn-tester/js/examples/Playground/RNTesterPlayground.jswith the linked reproducer. -
Build and install the Android release variant so that the JavaScript bundle and local assets are packaged in the APK:
./gradlew --no-daemon --max-workers=1 :packages:rn-tester:android:app:assembleRelease adb install -r packages/rn-tester/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
-
Open the RNTester Playground:
adb shell "am start -a android.intent.action.VIEW -d 'rntester://example/PlaygroundExample' com.facebook.react.uiapp" -
After approximately one second, observe the first frame containing A and B. A shows its gray placeholder while B already shows the same image.
-
Tap Run again to repeat.
The first commit inserts image A, four nested vote-list rows with real local images, and image B. A second commit adds 20 more rows. The list is scrollable.
React Native Version
0.88.0
Tested from 0.88-stable commit:
99d4ed39851392b4c65fd38fe3812669cb493376
The same underlying behavior was also observed on main at:
b8ec87e457c72377dab85599e1ae8c176eb242e7
Affected Platforms
Select:
Runtime - Android
Areas
Select:
Fabric - The New Renderer
Output of npx @react-native-community/cli info
The command was run from the RNTester monorepo checkout. The CLI does not infer RNTester's workspace packages or Gradle architecture setting correctly, so it reports package entries as Not Found and newArchEnabled: false. The actual RNTester Android configuration used for the reproduction has newArchEnabled=true.
System:
OS: macOS 27.0.1
CPU: (10) arm64 Apple M5
Memory: 64.22 MB / 24.00 GB
Shell:
version: 4.9.3
path: /opt/homebrew/bin/fish
Binaries:
Node:
version: 22.22.3
path: ~/.local/state/fnm_multishells/34159_1791960164938/bin/node
Yarn:
version: 1.22.22
path: ~/.local/state/fnm_multishells/34159_1791960164938/bin/yarn
npm:
version: 10.9.8
path: ~/.local/state/fnm_multishells/34159_1791960164938/bin/npm
Watchman:
version: 2026.07.27.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /Users/admin/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 27.0
- iOS 27.0
- macOS 27.0
- tvOS 27.0
- visionOS 27.0
- watchOS 27.0
Android SDK: Not Found
IDEs:
Android Studio: 2026.2 AI-262.9437.185.2621.16467767
Xcode:
version: 27.0/27A266a
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.20.1
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react: Not Found
react-native: Not Found
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: false
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false
Stacktrace or Logs
No crash or JavaScript error is logged.
Final RNTesterPlayground reproduction on React Native 0.88.0:
10/10 screen recordings captured A's placeholder while B already showed the image.
B-to-A display delays:
78, 67, 67, 62, 10, 75, 77, 74, 68, 75 ms
Median: approximately 71 ms; 9/10 runs lasted 62–78 ms.
MANDATORY Reproducer
https://github.com/fairysword/react-native/blob/9bc67419dfbde083cf258af46aa27f504836034c/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
Screenshots

Activity
fairysword commentedon Oct 17, 2026
I have pushed a prototype fix for discussion:
fairysword@ddddf2c
It preserves Fresco's existing posted UI-thread delivery path, while allowing pending results from opted-in React Native Image controllers to be applied during window pre-draw. Each callback still runs once and preserves posting order.
The focused unit tests pass locally. This is a prototype rather than a proposed final design, so feedback on the integration point would be appreciated. A feature flag can be added later if this approach is accepted.