From c9a098788c9b81082be6fe83d43517b0421bc65d Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Thu, 4 Jun 2026 15:25:05 -0700 Subject: [PATCH] Expose the bridgeless performance logger and post RCTJavaScriptDidLoadNotification (#57083) Summary: In bridgeless mode, `RCTInstance` records native startup timings in a per-instance `RCTPerformanceLogger`, but that logger was never reachable from app code: `RCTBridgeProxy.performanceLogger` returned nil and `RCTJavaScriptDidLoadNotification` was never posted. Startup-perf consumers that follow the long-standing pattern of reading `[bridge performanceLogger]` on `RCTJavaScriptDidLoadNotification` were therefore silently inert under bridgeless, dropping all native startup timings. This restores that pattern for bridgeless: - `RCTBridgeProxy` now holds a real `performanceLogger` property, injected by `RCTInstance`, instead of returning nil. - `RCTInstance` posts `RCTJavaScriptDidLoadNotification` (on the main thread, with the bridge proxy in `userInfo[@"bridge"]`) once the JS bundle has loaded, mirroring the legacy bridge. No change to the legacy bridge path. Changelog: [iOS][Fixed] - Expose the bridgeless performance logger via `RCTBridgeProxy` and post `RCTJavaScriptDidLoadNotification`, so native startup-perf consumers work in bridgeless Reviewed By: christophpurrer Differential Revision: D107542363 --- .../react-native/React/Base/RCTBridgeProxy.h | 8 ++++++++ .../react-native/React/Base/RCTBridgeProxy.mm | 8 ++------ .../platform/ios/ReactCommon/RCTInstance.mm | 17 ++++++++++++++++- .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/react-native/React/Base/RCTBridgeProxy.h b/packages/react-native/React/Base/RCTBridgeProxy.h index ef392aad70a3..274e48c65142 100644 --- a/packages/react-native/React/Base/RCTBridgeProxy.h +++ b/packages/react-native/React/Base/RCTBridgeProxy.h @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN @class RCTBundleManager; @class RCTCallableJSModules; @class RCTModuleRegistry; +@class RCTPerformanceLogger; @class RCTViewRegistry; @interface RCTBridgeProxy : NSProxy @@ -21,6 +22,13 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; +/** + * The performance logger for this React instance. In bridgeless mode it is + * injected by RCTInstance so that consumers reading `[bridge performanceLogger]` + * on `RCTJavaScriptDidLoadNotification` keep working; previously it returned nil. + */ +@property (nonatomic, strong, nullable) RCTPerformanceLogger *performanceLogger; + - (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry moduleRegistry:(RCTModuleRegistry *)moduleRegistry bundleManager:(RCTBundleManager *)bundleManager diff --git a/packages/react-native/React/Base/RCTBridgeProxy.mm b/packages/react-native/React/Base/RCTBridgeProxy.mm index d2268f9768c7..d09514d6e6a4 100644 --- a/packages/react-native/React/Base/RCTBridgeProxy.mm +++ b/packages/react-native/React/Base/RCTBridgeProxy.mm @@ -41,6 +41,8 @@ @implementation RCTBridgeProxy { void *_runtime; } +@synthesize performanceLogger = _performanceLogger; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-designated-initializers" - (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry @@ -213,12 +215,6 @@ - (BOOL)valid return NO; } -- (RCTPerformanceLogger *)performanceLogger -{ - [self logWarning:@"This method is not supported. Returning nil." cmd:_cmd]; - return nil; -} - - (void)reload { [self logError:@"Please use RCTReloadCommand instead. Nooping." cmd:_cmd]; diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index e744e34c7279..86bd9e4ee04f 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -115,6 +115,7 @@ @implementation RCTInstance { RCTPerformanceLogger *_performanceLogger; RCTDisplayLink *_displayLink; RCTTurboModuleManager *_turboModuleManager; + RCTBridgeProxy *_bridgeProxy; std::mutex _invalidationMutex; std::atomic _valid; RCTJSThreadManager *_jsThreadManager; @@ -351,6 +352,8 @@ - (void)_start runtime:_reactInstance->getJavaScriptContext() launchOptions:_launchOptions]; bridgeProxy.jsCallInvoker = jsCallInvoker; + bridgeProxy.performanceLogger = _performanceLogger; + _bridgeProxy = bridgeProxy; [RCTBridge setCurrentBridge:(RCTBridge *)bridgeProxy]; // Set up TurboModules @@ -606,8 +609,20 @@ - (void)_loadScriptFromSource:(RCTSource *)source waitUntilModuleSetupComplete(); } }; - auto afterLoad = [](jsi::Runtime &_) { + __weak __typeof(self) weakSelf = self; + auto afterLoad = [weakSelf](jsi::Runtime &) { [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTInstanceDidLoadBundle" object:nil]; + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf) { + RCTBridgeProxy *bridgeProxy = strongSelf->_bridgeProxy; + // afterLoad runs on the JS thread; post on main like the legacy bridge so + // RCTJavaScriptDidLoadNotification observers aren't invoked off-main. + RCTExecuteOnMainQueue(^{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:strongSelf + userInfo:bridgeProxy ? @{@"bridge" : bridgeProxy} : @{}]; + }); + } }; _reactInstance->loadScript(std::move(script), url, beforeLoad, afterLoad); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index ef3f2079032f..50d3e417ca4e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -725,6 +725,7 @@ interface RCTBridgeModuleDecorator : public NSObject { } interface RCTBridgeProxy : public NSProxy { + public @property (strong) RCTPerformanceLogger* performanceLogger; public virtual NSMethodSignature* methodSignatureForSelector:(SEL sel); public virtual id moduleForClass:(Class moduleClass); public virtual id moduleForName:lazilyLoadIfNecessary:(NSString* moduleName, BOOL lazilyLoad); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 951775f5a1bf..14ab009b2bd3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -723,6 +723,7 @@ interface RCTBridgeModuleDecorator : public NSObject { } interface RCTBridgeProxy : public NSProxy { + public @property (strong) RCTPerformanceLogger* performanceLogger; public virtual NSMethodSignature* methodSignatureForSelector:(SEL sel); public virtual id moduleForClass:(Class moduleClass); public virtual id moduleForName:lazilyLoadIfNecessary:(NSString* moduleName, BOOL lazilyLoad); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 5226973262d9..018321623dcd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -725,6 +725,7 @@ interface RCTBridgeModuleDecorator : public NSObject { } interface RCTBridgeProxy : public NSProxy { + public @property (strong) RCTPerformanceLogger* performanceLogger; public virtual NSMethodSignature* methodSignatureForSelector:(SEL sel); public virtual id moduleForClass:(Class moduleClass); public virtual id moduleForName:lazilyLoadIfNecessary:(NSString* moduleName, BOOL lazilyLoad);