Repository navigation
TurboModuleContext (default since 8.14) adds ~5 ms of CPU to every RNSentry call on iOS through native scope persistence #6886
Description
What React Native libraries do you use?
React Navigation, Hermes, RN New Architecture
Are you using sentry.io or on-premise?
sentry.io (SaS)
Are you using any other error monitoring solution alongside Sentry?
No
Other Error Monitoring Solution Name
No response
@sentry/react-native SDK Version
8.7.0 → 8.25.0 (measured)
How does your development environment look like?
System:
OS: macOS 26.6.2
CPU: (14) arm64 Apple M4 Pro
Memory: 24.00 GB
Binaries:
Node: 26.5.0
npm: 11.17.0
Managers:
CocoaPods: 1.16.2
npmPackages:
"@react-native-community/cli": 20.1.0
react: 19.2.3
react-native: 0.86.0
expo: 57.0.8
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Native SDKs (from @sentry/react-native 8.27.0): sentry-cocoa 9.29.0, sentry-android 8.57.0
Measurements below were taken on @sentry/react-native 8.25.0 (sentry-cocoa 9.26.1).
Sentry.init()
Sentry.init({
dsn: CONFIG.SENTRY_DSN,
tracesSampleRate: 1.0,
enableAutoPerformanceTracing: true,
enableUserInteractionTracing: true,
integrations: [
navigationIntegration, // Sentry.reactNavigationIntegration()
tracingIntegration, // Sentry.reactNativeTracingIntegration()
// + a few JS-only integrations (console, breadcrumbs, event filters)
],
environment: CONFIG.ENVIRONMENT,
release: `${pkg.name}@${semanticVersion}`,
dist: buildNumber,
enableLogs: true,
// Native SDK is initialized early in AppDelegate / Application.onCreate
autoInitializeNativeSdk: false,
_experiments: {
profilingOptions: {
profileSessionSampleRate: 0.1,
lifecycle: 'trace',
},
},
});Full file: https://github.com/Expensify/App/blob/main/src/setup/telemetry/setupSentry.ts
Steps to Reproduce
- Use @sentry/react-native 8.14 or later with enableNative: true (default), so turboModuleContextIntegration() is added to the default integrations and wraps the RNSentry TurboModule.
- On iOS, call any wrapped RNSentry method in a loop, for example 300 sequential await RNSentry.fetchNativeFramesDelay(...) calls (any non-skipped method behaves the same), and measure the app process CPU time (ps -o time= -p before and after).
- Repeat with the integration filtered out:
integrations: (defaults) => [...defaults.filter((i) => i.name !== 'TurboModuleContext'), ...ourIntegrations], - For a real-world view, switch between bottom tabs 40 times (3 s apart) in an app with reactNavigationIntegration + tracing, and compare process CPU with and without the integration.
Expected Result
Crash-context instrumentation of the SDK's own RNSentry module costs roughly the same as the call it wraps, so the SDK's routine calls (fetchNativeFrames, fetchNativeFramesDelay, popTimeToDisplayFor, fetchNativeDeviceContexts, ...) stay cheap.
Actual Result
On iOS every wrapped RNSentry call becomes several native scope writes, and each native scope change is persisted to disk by the watchdog-termination scope observer.
Measured with a release bundle on the iOS simulator and a debug APK on the Android emulator, 2 runs per variant, app process CPU sampled every second:
iOS simulator Android emulator
CPU per RNSentry call ~4.7 ms vs ~0.03 ms ~1.0 ms vs ~0.05 ms
40 tab switches, total CPU 28.7 / 29.7 s vs 44.6 / 43.8 s vs
19.6 / 17.6 s 41.4 / 41.7 s
per tab switch +0.27 s (+57%) +0.07 s (+6.5%)
idle CPU no difference no difference
(first value: integration on, the default; second: TurboModuleContext filtered out)
On a physical iPhone 17 Pro (release build, synthetic burst of back-to-back calls) the difference was ~19 ms vs ~0.2 ms of CPU per call.
A single bottom-tab switch makes about 54 RNSentry calls, all from the SDK itself (mostly fetchNativeFrames, popTimeToDisplayFor, fetchNativeFramesDelay, fetchNativeDeviceContexts, setActiveSpanId). We do not pass any modules to the integration.
Where the cost comes from (turboModuleTracker.js, 8.27.0):
- Every wrapped call writes turbo_module context and two tags to the JS scope at start and again at end (syncToScope() / clearScope()). Each write is forwarded to the native scope via RNSentry.setContext / RNSentry.setTag, so one RNSentry call produces about 6 native scope writes.
- On iOS, each native scope change is persisted by the watchdog-termination scope observer. The CPU time is spent on the io.sentry.watchdog-termination-tracking.fields-processor queue, writing the context/tags file. Idle CPU is unaffected.
- ignoreTurboModules defaults to ['RNSentry'], so with default options the only output is the turbo_module tag on crash events naming the SDK method that was in flight.
Possible fixes:
- Don't wrap RNSentry by default, or skip its frequently called read-only methods (fetchNativeFrames*, popTimeToDisplayFor, fetchNativeDeviceContexts).
- Coalesce the native scope sync, or persist watchdog fields with debouncing on the cocoa side (similar to how breadcrumb persistence was changed in fix: Move breadcrumb file I/O off caller sentry-cocoa#8653).
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsNo status
Activity
linear-code commentedon Oct 13, 2026
RN-824
alwx commentedon Oct 14, 2026
Thanks for reporting, @arekm213. We've seen your request and added it to our triage queue.
We'll review it shortly and follow up if we need more information or once next steps are clearer.
fix(core): Stop wrapping RNSentry in TurboModuleContext by default (#…
github-actions commentedon Oct 22, 2026
A PR closing this issue has just been released 🚀
This issue was referenced by PR #6921, which was included in the 8.32.0 release.