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:
@@ -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
|
||||||
window.scrollTo(0, scrollY)
|
// Deferred so that screens re-mounted by LRU eviction have
|
||||||
|
// time to render their content before we scroll.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user