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!

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.

1 participant