web: LRU screen cache to bound memory in long sessions (#10063)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-03-18 08:50:48 -07:00
committed by GitHub
parent 317ce2b3da
commit 530afe87c7
2 changed files with 59 additions and 14 deletions
+4
View File
@@ -42,7 +42,11 @@ export function useWebScrollRestoration() {
() => ({ () => ({
focus(e: EventArg<'focus', boolean | undefined, unknown>) { focus(e: EventArg<'focus', boolean | undefined, unknown>) {
const scrollY = state.scrollYs.get(e.target) ?? 0 const scrollY = state.scrollYs.get(e.target) ?? 0
// Deferred so that screens re-mounted by LRU eviction have
// time to render their content before we scroll.
requestAnimationFrame(() => {
window.scrollTo(0, scrollY) window.scrollTo(0, scrollY)
})
state.focusedKey = e.target ?? null state.focusedKey = e.target ?? null
}, },
}), }),
@@ -42,6 +42,10 @@ import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav' import {DesktopRightNav} from './desktop/RightNav'
// On web, only this many screens (beyond Home + focused) stay mounted.
// Older screens are unmounted to prevent memory growth during long sessions.
const WEB_MAX_CACHED_SCREENS = 5
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
requireAuth?: boolean requireAuth?: boolean
} }
@@ -105,6 +109,8 @@ function NativeStackNavigator({
) )
// --- our custom logic starts here --- // --- our custom logic starts here ---
// Web LRU: tracks route keys in most-recently-focused order
const lruKeysRef = React.useRef<string[]>([])
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const activeRoute = state.routes[state.index] const activeRoute = state.routes[state.index]
const activeDescriptor = descriptors[activeRoute.key] const activeDescriptor = descriptors[activeRoute.key]
@@ -126,19 +132,54 @@ function NativeStackNavigator({
if (onboardingState.isActive) { if (onboardingState.isActive) {
return <Onboarding /> return <Onboarding />
} }
const newDescriptors: typeof descriptors = {} // On web, limit how many screens stay mounted to prevent memory growth.
for (let key in descriptors) { // Home is always pinned, the focused screen is always mounted, and the
const descriptor = descriptors[key] // most recently visited screens are kept up to WEB_MAX_CACHED_SCREENS.
const requireAuth = descriptor.options.requireAuth ?? false // Evicted screens render a lightweight placeholder — the route stays in
newDescriptors[key] = { // state so browser back/forward still works; the component just re-mounts.
...descriptor, let finalDescriptors = descriptors
render() { if (IS_WEB) {
if (requireAuth && !hasSession) { const focusedKey = activeRoute.key
return <View />
} else { // Update LRU: move focused key to front
return descriptor.render() const lru = lruKeysRef.current
const idx = lru.indexOf(focusedKey)
if (idx > 0) {
lru.splice(idx, 1)
lru.unshift(focusedKey)
} else if (idx === -1) {
lru.unshift(focusedKey)
}
// Remove keys for routes no longer in the stack
const routeKeySet = new Set(state.routes.map(r => r.key))
lruKeysRef.current = lruKeysRef.current.filter(k => routeKeySet.has(k))
// Build mount set: Home (pinned) + focused + N most recent
const mountSet = new Set<string>()
mountSet.add(focusedKey)
const homeKey = state.routes.find(r => r.name === 'Home')?.key
if (homeKey) mountSet.add(homeKey)
let cached = 0
for (const key of lruKeysRef.current) {
if (cached >= WEB_MAX_CACHED_SCREENS) break
if (!mountSet.has(key)) {
mountSet.add(key)
cached++
}
}
// Evicted screens get a lightweight placeholder instead of their full tree
finalDescriptors = {} as typeof descriptors
for (const key in descriptors) {
if (mountSet.has(key)) {
finalDescriptors[key] = descriptors[key]
} else {
finalDescriptors[key] = {
...descriptors[key],
render: () => <View />,
}
} }
},
} }
} }
@@ -153,7 +194,7 @@ function NativeStackNavigator({
{...rest} {...rest}
state={state} state={state}
navigation={navigation} navigation={navigation}
descriptors={descriptors} descriptors={finalDescriptors}
describe={describe} describe={describe}
/> />
</View> </View>