Skip to content

[Android][Fabric] A decoded local image can miss the current frame during a long mount #59158

Open

Description

@fairysword

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
Loading

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

  1. Check out React Native 0.88-stable at 99d4ed39851392b4c65fd38fe3812669cb493376.

  2. Replace packages/rn-tester/js/examples/Playground/RNTesterPlayground.js with the linked reproducer.

  3. 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
  4. Open the RNTester Playground:

    adb shell "am start -a android.intent.action.VIEW -d 'rntester://example/PlaygroundExample' com.facebook.react.uiapp"
  5. After approximately one second, observe the first frame containing A and B. A shows its gray placeholder while B already shows the same image.

  6. 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

Image

Activity

  1. fairysword commented on Oct 17, 2026

    @fairysword
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions