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
@@ -42,6 +42,10 @@ import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav'
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 & {
requireAuth?: boolean
}
@@ -105,6 +109,8 @@ function NativeStackNavigator({
)
// --- 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 activeRoute = state.routes[state.index]
const activeDescriptor = descriptors[activeRoute.key]
@@ -126,19 +132,54 @@ function NativeStackNavigator({
if (onboardingState.isActive) {
return <Onboarding />
}
const newDescriptors: typeof descriptors = {}
for (let key in descriptors) {
const descriptor = descriptors[key]
const requireAuth = descriptor.options.requireAuth ?? false
newDescriptors[key] = {
...descriptor,
render() {
if (requireAuth && !hasSession) {
return <View />
} else {
return descriptor.render()
// On web, limit how many screens stay mounted to prevent memory growth.
// Home is always pinned, the focused screen is always mounted, and the
// most recently visited screens are kept up to WEB_MAX_CACHED_SCREENS.
// Evicted screens render a lightweight placeholder — the route stays in
// state so browser back/forward still works; the component just re-mounts.
let finalDescriptors = descriptors
if (IS_WEB) {
const focusedKey = activeRoute.key
// Update LRU: move focused key to front
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}
state={state}
navigation={navigation}
descriptors={descriptors}
descriptors={finalDescriptors}
describe={describe}
/>
</View>