Skip to content

Preserve lazy ViewManager initialization in UIManager - #58097

Open
fairysword wants to merge 1 commit into
react:mainfrom
fairysword:fix/uimanager-lazy-viewmanager-initialization
Open

fairysword wants to merge 1 commit into
react:mainfrom
fairysword:fix/uimanager-lazy-viewmanager-initialization

Conversation

@fairysword

@fairysword fairysword commented Aug 24, 2026 •

Copy link
Copy Markdown

Summary:

  • Preserve lazy ViewManager property descriptors when composing UIManager.
  • Avoid eagerly invoking lazy ViewManager getters and unnecessary getConstantsForViewManager calls during Bridgeless initialization.
  • Add regression coverage for lazy, own, enumerable and one-time getter behavior.
  • Platform scope: This targets the Android Bridgeless lazy ViewManager path. iOS does not currently expose this lazy getter path, so no iOS runtime behavior or performance change is expected.

Changelog:

[ANDROID] [FIXED] - Avoid unnecessary getConstantsForViewManager calls during UIManager initialization.

Test Plan:

  • yarn flow-check
  • yarn test packages/react-native/Libraries/ReactNative --runInBand
  • yarn lint --quiet
  • yarn format-check
  • Verified with Android RNTester offlineDebug that startup no longer triggers eager getConstantsForViewManager calls.

Copy UIManager implementation property descriptors instead of spreading UIManagerImpl, so Bridgeless initialization does not eagerly invoke lazy ViewManager getters or trigger unnecessary getConstantsForViewManager calls.

Add regression coverage verifying that ViewManager descriptors remain lazy, own, and enumerable, and that each getter is evaluated only once on first access.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 24, 2026
@fairysword

fairysword commented Aug 24, 2026 •

Copy link
Copy Markdown
Author

UIManager lazy ViewManager initialization optimization

Test environment:

  • RNTester offlineDebug
  • Debug native code with a Hermes bundle loaded from APK assets; Metro/dev support disabled
  • Same Android device (V2338A, Android 16)
  • logcat cleared and the app force-stopped before each cold start
  • The bundle task was forced to rerun before each APK was installed

offlineDebug is a local RNTester configuration created only for these manual performance measurements; it is not an upstream RNTester configuration and is not part of this PR. The proposed patch's automated regression coverage is UIManager-test.js and does not depend on this configuration or on the instrumentation below.

Metric definitions:

  • Bundle Load = timestamp(RUN_JS_BUNDLE_END) - timestamp(RUN_JS_BUNDLE_START)
  • CONTENT_APPEARED = timestamp(CONTENT_APPEARED) - timestamp(RUN_JS_BUNDLE_START)

1. Temporary ReactInstance instrumentation

The following debug-only instrumentation was added locally to ReactInstance.kt. It is not part of the proposed patch.

import android.util.Log

private fun createConstants(
    viewManagers: List<ViewManager<in Nothing, in Nothing>>,
    customDirectEvents: MutableMap<String, Any>?,
): MutableMap<String, Any> {
  val startNanos = System.nanoTime()
  // Existing implementation...
  try {
    return UIManagerModuleConstantsHelper.createConstants(
        viewManagers,
        null,
        customDirectEvents,
    )
  } finally {
    // Existing marker cleanup...
    if (BuildConfig.DEBUG) {
      Log.d(
          "RNUIManagerPerf",
          "getConstants(eagerViewManagers=${viewManagers.size}) took ${(System.nanoTime() - startNanos) / 1_000_000.0}ms",
      )
    }
  }
}

private fun getConstantsForViewManager(
    viewManager: ViewManager<*, *>,
    customDirectEvents: MutableMap<String, Any>,
): NativeMap {
  val startNanos = System.nanoTime()
  // Existing implementation...
  try {
    val viewManagerConstants =
        UIManagerModuleConstantsHelper.createConstantsForViewManager(
            viewManager,
            null,
            null,
            null,
            customDirectEvents,
        )
    return Arguments.makeNativeMap(viewManagerConstants)
  } finally {
    // Existing trace cleanup...
    if (BuildConfig.DEBUG) {
      Log.d(
          "RNUIManagerPerf",
          "getConstantsForViewManager(${viewManager.name}) took ${(System.nanoTime() - startNanos) / 1_000_000.0}ms",
      )
    }
  }
}

2. Before/after logs

Before: object spread

With ...UIManagerImpl, importing UIManager reads every enumerable lazy ViewManager property. This synchronously calls getConstantsForViewManager for all 20 registered lazy ViewManagers during startup.

D/RNUIManagerPerf: getConstants(eagerViewManagers=0) took 0.937031ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTSafeAreaView) took 15.836355ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTModalHostView) took 6.342136ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidProgressBar) took 4.09651ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidHorizontalScrollView) took 6.418177ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTImageView) took 5.935573ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTText) took 5.849114ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidHorizontalScrollContentView) took 7.902032ms
D/RNUIManagerPerf: getConstantsForViewManager(RNTMyLegacyNativeView) took 3.298698ms
D/RNUIManagerPerf: getConstantsForViewManager(RNTReportFullyDrawnView) took 3.527865ms
D/RNUIManagerPerf: getConstantsForViewManager(UnimplementedNativeView) took 3.425677ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTScrollView) took 6.097032ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTView) took 7.669635ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidDrawerLayout) took 3.646875ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidSwitch) took 3.287812ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidSwipeRefreshLayout) took 3.423437ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidPopupMenu) took 2.970105ms
D/RNUIManagerPerf: getConstantsForViewManager(RNTMyNativeView) took 2.043542ms
D/RNUIManagerPerf: getConstantsForViewManager(AndroidTextInput) took 6.461354ms
D/RNUIManagerPerf: getConstantsForViewManager(RCTSelectableText) took 3.281875ms
D/RNUIManagerPerf: getConstantsForViewManager(VirtualView) took 2.945833ms

The 20 getConstantsForViewManager calls took 104.46 ms cumulatively.

Startup timing from the same run:

Bundle Load: 161ms
CONTENT_APPEARED: 356ms

After: property descriptor composition

After preserving the lazy property descriptors instead of reading their values, startup no longer calls getConstantsForViewManager:

D/RNUIManagerPerf: getConstants(eagerViewManagers=0) took 3.361563ms

Result:

  • getConstantsForViewManager startup calls: 20 → 0
  • Cumulative synchronous ViewManager configuration work: 104.46 ms → 0 ms

Startup timing from the same run:

Bundle Load: 49ms
CONTENT_APPEARED: 289ms

The marker timings above are one cold-start sample and are included as supporting evidence. The deterministic regression signal is the removal of all 20 synchronous getConstantsForViewManager calls during UIManager module initialization.

@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Aug 24, 2026
@fairysword

Copy link
Copy Markdown
Author

Hi maintainers — gentle ping on this PR, now open for just over two weeks. It preserves lazy ViewManager initialization in Bridgeless UIManager and includes a regression test verifying that getters are not eagerly invoked at module load. Happy to address any feedback or add coverage if helpful. Thanks!

@javache
javache requested a review from rubennorte September 9, 2026 11:58
@fairysword

Copy link
Copy Markdown
Author

Hi @rubennorte — gentle follow-up on the review request from last week. This PR is still current and ready for review, with a regression test covering the eager ViewManager getter behavior. I’m happy to address any feedback or adjust the approach. Thanks for your time!

@gadcam

gadcam commented Sep 24, 2026

Copy link
Copy Markdown

TLDR: With the patch, the first screen appears about 58 ms sooner

We applied this change as a patch to React Native 0.86.3 in our app and measured it.

Setup

  • Pixel 8a, Android 17
  • Release build: Hermes, arm64-v8a, compiled with speed-profile
  • New architecture, bridgeless, useNativeViewConfigsInBridgelessMode on (the default)
  • Cold starts, app data kept between launches

Hermes sampling profiler (HermesSamplingProfiler, ~10 ms sampling, from app start until the first screen is drawn)

Launches getConstantsForViewManager per launch
Without the patch 23 27 ms
With the patch 8 not sampled

Without the patch, every sample of getConstantsForViewManager had this stack:

getConstantsForViewManager   BridgelessUIManager.js
<- get                       BridgelessUIManager.js (lazy ViewManager getter)
<- getValue                  defineLazyObjectProperty.js
<- hermesBuiltinCopyDataProperties
<- UIManager.js              `...UIManagerImpl`
<- NativeComponentRegistry.js
<- ViewNativeComponent.js

Launch time: 10 cold starts per build, run back to back, same device.

First frame (median) First screen drawn (median) First screen drawn (min–max)
Without the patch 247 ms 626 ms 572–685 ms
With the patch 246 ms 568 ms 548–615 ms

"First frame" is TotalTime from am start -W. "First screen drawn" is Activity.reportFullyDrawn(), called when the app hides its splash screen.

We saw no crash or JS error at launch with the patch.

@gadcam

gadcam commented Sep 24, 2026

Copy link
Copy Markdown

Also when I asked Claude how it happened, it told me the following.

It is an accident between two changes made a year apart:

  1. Dec 2022, 455ca8c
    ("Make UIManager.measure* compatible with Fabric"): UIManager.js became
    { ...UIManagerImpl, measure, measureInWindow, ... } so that measure* work on Fabric
    without changes in apps. At that point UIManagerImpl only held plain methods and constants,
    so a spread was harmless.
  2. Jan 2024, cc1258c
    ("Make nvc interop layer lazy", '[skip ci] [Venice][Android] Make nvc interop layer lazy #42218): BridgelessUIManager stopped fetching the settings
    of view managers registered as lazy, and exposed them as lazy getters instead
    (defineLazyObjectProperty). That change only touched BridgelessUIManager.js and native
    code, not UIManager.js. The spread one level up has read every getter at startup ever
    since, which undoes the laziness on Android.

This only runs when useNativeViewConfigsInBridgelessMode is on, and it is on by default for
new-architecture apps.

@david-mobile1

Copy link
Copy Markdown

Approved. This is a critical fix which improves my app startup time about 28%

@fairysword

Copy link
Copy Markdown
Author

Hi @javache — could you please advise whether this PR should remain with @rubennorte or be reassigned? The review request has been pending since September 9, with no formal review or import yet. The PR includes regression coverage, and other contributors have also reported that it improves first-screen startup. I’m happy to make any changes needed. Thank you!

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants