diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index f2a809361ee4..86166278108b 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -23,6 +23,7 @@ import { appAtomRegistry } from "./state/atom-registry"; import { OverlayPortalHost } from "./components/OverlayPortal"; import { shouldHandleAppLink } from "./lib/appLinking"; import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme"; +import { useUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc"; import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator"; import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider"; import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl"; @@ -57,6 +58,8 @@ function SplashScreenCoordinator() { } export default function App() { + useUiRuntimeMemoryWarningGc(); + return ( diff --git a/apps/mobile/src/lib/useUiRuntimeMemoryWarningGc.ts b/apps/mobile/src/lib/useUiRuntimeMemoryWarningGc.ts new file mode 100644 index 000000000000..8e702f25aa17 --- /dev/null +++ b/apps/mobile/src/lib/useUiRuntimeMemoryWarningGc.ts @@ -0,0 +1,24 @@ +import { useEffect } from "react"; +import { AppState } from "react-native"; +import { scheduleOnUI } from "react-native-worklets"; + +function collectUiRuntimeGarbage() { + "worklet"; + // Hermes exposes `gc` on the worklets UI runtime; skip quietly on runtimes that do not. + const gc = (globalThis as { gc?: () => void }).gc; + if (typeof gc === "function") gc(); +} + +/** + * React Native only collects the main JS runtime on an iOS memory warning. The + * worklets UI runtime is a separate Hermes heap that holds native shadow nodes and + * serializables alive until its own GC runs, so collect it on the same warning. + */ +export function useUiRuntimeMemoryWarningGc() { + useEffect(() => { + const subscription = AppState.addEventListener("memoryWarning", () => { + scheduleOnUI(collectUiRuntimeGarbage); + }); + return () => subscription.remove(); + }, []); +}