Improve caching

This commit is contained in:
2026-07-21 18:45:59 +10:00
parent 92c61abcfe
commit 7d134ff8c9
24 changed files with 519 additions and 105 deletions

View File

@@ -15,6 +15,7 @@ import {
} from 'vue-router'
import type {
NativeDirection,
NativeEvictionReason,
NativeGestureKind,
NativeNavigationOptions,
NativePlatformAdapter,
@@ -81,6 +82,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
readonly activeKey
readonly transaction
readonly canGoBack
readonly cacheStats
private readonly mutableEntries = shallowRef<NativeViewEntry[]>([])
private readonly mutableActiveKey = ref('')
@@ -95,14 +97,32 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
private removeAfterEach?: () => void
private platformCleanup?: () => void
private pendingPop?: (failure?: NavigationFailure | void) => void
private totalEvictions = 0
private lastEviction?: { key: string; route: string; reason: NativeEvictionReason }
private memoryPressureCleanup?: () => void
constructor(options: NativeRouterOptions) {
this.router = options.router
this.maxInactive = options.cache?.maxInactive ?? 8
this.maxInactive = Math.max(0, options.cache?.maxInactive ?? 4)
this.platform = options.platform
this.entries = computed(() => this.mutableEntries.value)
this.activeKey = computed(() => this.mutableActiveKey.value)
this.transaction = computed(() => this.mutableTransaction.value)
this.cacheStats = computed(() => {
const entries = this.mutableEntries.value
const mounted = entries.filter((entry) => entry.mounted)
const inactive = mounted.filter((entry) => entry.key !== this.mutableActiveKey.value)
return {
maxInactive: this.maxInactive,
descriptors: entries.length,
mounted: mounted.length,
inactive: inactive.length,
pinned: inactive.filter((entry) => entry.route.meta.native?.cache === 'pin').length,
evicted: entries.filter((entry) => !entry.mounted).length,
totalEvictions: this.totalEvictions,
lastEviction: this.lastEviction,
}
})
this.canGoBack = computed(() => {
const transaction = this.mutableTransaction.value
const committingForwardEntry = transaction
@@ -141,6 +161,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (cleanup) this.platformCleanup = cleanup
})
}
if (typeof window !== 'undefined') {
const trimForPressure = () => this.trimCache({ reason: 'memory-pressure' })
window.addEventListener('memorypressure', trimForPressure)
this.memoryPressureCleanup = () => window.removeEventListener('memorypressure', trimForPressure)
}
}
async preload(to: RouteLocationRaw) {
@@ -221,6 +246,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
target.route = route
target.mounted = true
target.status = 'inactive'
target.evictionReason = undefined
this.touchEntries()
}
} else {
@@ -236,6 +262,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
target.status = 'preview'
target.synthetic = false
target.lastUsed = now()
target.evictionReason = undefined
this.touchEntries()
} else {
target = entryFor(route, 'preview')
@@ -309,7 +336,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (!this.isCurrent(current.id)) return !failed
if (failed) {
await this.animateProgress(0, 0)
if (this.isCurrent(current.id)) this.finalizeCancelled(current)
if (this.isCurrent(current.id)) this.finalizeCancelled(current, true)
return false
}
this.finalizeCommitted(current)
@@ -326,6 +353,16 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
this.finalizeCancelled(current)
}
trimCache(options: { includePinned?: boolean; reason?: NativeEvictionReason } = {}) {
const reason = options.reason ?? 'trimmed'
for (const entry of this.mutableEntries.value) {
if (entry.key === this.mutableActiveKey.value || !entry.mounted || entry.status !== 'inactive') continue
if (!options.includePinned && entry.route.meta.native?.cache === 'pin') continue
this.evictEntry(entry, reason)
}
this.touchEntries()
}
registerPresentation(definition: NativePresentationDefinition) {
this.presentations.set(definition.name, definition)
}
@@ -350,6 +387,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
dispose() {
this.removeAfterEach?.()
this.platformCleanup?.()
this.memoryPressureCleanup?.()
}
private activeEntry() {
@@ -414,15 +452,17 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
velocity: 0,
phase: 'settling',
}
if (failed) this.finalizeCancelled(transaction)
if (failed) this.finalizeCancelled(transaction, true)
else this.finalizeCommitted(transaction)
}
private finalizeCancelled(transaction: NativeTransaction) {
private finalizeCancelled(transaction: NativeTransaction, evictTarget = false) {
if (!this.isCurrent(transaction.id)) return
this.removePreview(transaction.toKey)
this.clearTransaction()
if (evictTarget) this.discardTarget(transaction.toKey, 'navigation-rejected')
else this.removePreview(transaction.toKey)
this.markStatuses()
this.enforceCache()
}
private finalizeCommitted(transaction: NativeTransaction) {
@@ -430,9 +470,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
const target = this.entryByKey(transaction.toKey)
if (target && this.router.currentRoute.value.fullPath !== target.route.fullPath) {
// A redirect is already authoritative; discard only the stale preview.
this.removePreview(transaction.toKey)
this.clearTransaction()
this.discardTarget(transaction.toKey, 'navigation-rejected')
this.markStatuses()
this.enforceCache()
return
}
if (target) {
@@ -486,11 +527,13 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
target.status = 'active'
target.committed = true
target.lastUsed = now()
target.evictionReason = undefined
this.touchEntries()
}
this.mutableActiveKey.value = target.key
this.acceptHistory(target, transaction)
this.markStatuses()
if (!transaction) this.enforceCache()
}
private acceptHistory(target: NativeViewEntry, transaction: NativeTransaction | null) {
@@ -542,16 +585,49 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
}
}
private discardTarget(key: string, reason: NativeEvictionReason) {
const entry = this.entryByKey(key)
if (!entry || entry.key === this.mutableActiveKey.value) return
if (!entry.committed || entry.synthetic) {
this.mutableEntries.value = this.mutableEntries.value.filter((candidate) => candidate.key !== key)
return
}
this.evictEntry(entry, reason)
}
private evictEntry(entry: NativeViewEntry, reason: NativeEvictionReason) {
if (!entry.mounted || entry.key === this.mutableActiveKey.value) return
entry.mounted = false
entry.status = 'evicted'
entry.evictionReason = reason
this.totalEvictions += 1
this.lastEviction = { key: entry.key, route: entry.route.fullPath, reason }
}
private shouldRetainInactive(entry: NativeViewEntry) {
const policy = entry.route.meta.native?.cache
if (policy === false) return false
if (policy === 'pin') return true
if (this.mutableHistoryKeys.value.includes(entry.key)) return true
return entry.route.meta.native?.siblingHistory === 'replace'
}
private enforceCache() {
const inactive = this.mutableEntries.value
.filter((entry) => entry.key !== this.mutableActiveKey.value && entry.mounted && entry.status === 'inactive')
.sort((a, b) => b.lastUsed - a.lastUsed)
for (const entry of inactive.slice(this.maxInactive)) {
if (entry.route.meta.native?.cache === false || inactive.length > this.maxInactive) {
entry.mounted = false
entry.status = 'evicted'
for (const entry of inactive) {
if (!this.shouldRetainInactive(entry)) {
const reason: NativeEvictionReason = entry.route.meta.native?.cache === false
? 'cache-disabled'
: 'popped'
this.evictEntry(entry, reason)
}
}
const retained = inactive.filter((entry) => entry.mounted && entry.route.meta.native?.cache !== 'pin')
for (const entry of retained.slice(this.maxInactive)) {
this.evictEntry(entry, 'cache-limit')
}
this.touchEntries()
}