Fix frame pacing

This commit is contained in:
2026-07-21 21:59:26 +10:00
parent 7d134ff8c9
commit db864af147
10 changed files with 202 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
import {
computed,
nextTick,
ref,
shallowRef,
type App,
@@ -226,6 +227,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
let target: NativeViewEntry | undefined
let synthetic = false
let needsMount = false
const isBack = kind === 'pop' || kind === 'dismiss'
if (isBack) {
@@ -239,6 +241,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
target = entryFor(route, 'preview', true)
synthetic = true
needsMount = true
this.mutableEntries.value = [...this.mutableEntries.value, target]
} else if (!target.mounted) {
const route = await this.preload(target.route.fullPath)
@@ -247,6 +250,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
target.mounted = true
target.status = 'inactive'
target.evictionReason = undefined
needsMount = true
this.touchEntries()
}
} else {
@@ -255,8 +259,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (resolved.fullPath === from.route.fullPath) return null
const route = await loadRouteLocation(resolved)
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
target = this.findReusable(route.fullPath)
const replace = options.replace ?? (route.meta.native?.siblingHistory === 'replace')
target = replace ? this.findHistoryEntry(route.fullPath) : undefined
target ??= this.findReusable(route.fullPath)
if (target) {
needsMount = !target.mounted
target.route = route
target.mounted = true
target.status = 'preview'
@@ -266,6 +273,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
this.touchEntries()
} else {
target = entryFor(route, 'preview')
needsMount = true
this.mutableEntries.value = [...this.mutableEntries.value, target]
}
}
@@ -300,6 +308,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
this.mutableTransaction.value = transaction
if (synthetic) target.synthetic = true
if (needsMount) {
await this.prepareMountedView()
if (!this.isCurrent(transaction.id)) return null
}
return transaction.id
}
@@ -353,6 +365,21 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
this.finalizeCancelled(current)
}
unload(to: RouteLocationRaw) {
const fullPath = this.router.resolve(to).fullPath
const transaction = this.mutableTransaction.value
let unloaded = 0
for (const entry of this.mutableEntries.value) {
if (entry.route.fullPath !== fullPath || !entry.mounted) continue
if (entry.key === this.mutableActiveKey.value) continue
if (entry.key === transaction?.fromKey || entry.key === transaction?.toKey) continue
this.evictEntry(entry, 'manual')
unloaded += 1
}
if (unloaded) this.touchEntries()
return unloaded
}
trimCache(options: { includePinned?: boolean; reason?: NativeEvictionReason } = {}) {
const reason = options.reason ?? 'trimmed'
for (const entry of this.mutableEntries.value) {
@@ -493,22 +520,33 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (!target) return true
if (transaction.kind === 'pop' || transaction.kind === 'dismiss') {
if (target.synthetic) return await this.router.replace(target.route.fullPath)
return await new Promise<NavigationFailure | void>((resolve) => {
this.pendingPop = resolve
this.router.back()
window.setTimeout(() => {
if (this.pendingPop === resolve) {
this.pendingPop = undefined
resolve()
}
}, 1200)
})
return await this.navigateHistory(-1)
}
if (transaction.replace) {
const history = this.mutableHistoryKeys.value
const targetIndex = history.lastIndexOf(target.key)
if (targetIndex >= 0 && targetIndex < history.length - 1) {
return await this.navigateHistory(targetIndex - (history.length - 1))
}
}
return transaction.replace
? await this.router.replace(target.route.fullPath)
: await this.router.push(target.route.fullPath)
}
private async navigateHistory(delta: number) {
return await new Promise<NavigationFailure | void>((resolve) => {
this.pendingPop = resolve
this.router.go(delta)
window.setTimeout(() => {
if (this.pendingPop === resolve) {
this.pendingPop = undefined
resolve()
}
}, 1200)
})
}
private acceptRoute(to: RouteLocationNormalizedLoaded, _from: RouteLocationNormalizedLoaded) {
const transaction = this.mutableTransaction.value
let target = transaction ? this.entryByKey(transaction.toKey) : undefined
@@ -550,7 +588,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
return
}
if (transaction?.replace) {
this.mutableHistoryKeys.value = [...history.slice(0, -1), target.key]
const targetIndex = history.lastIndexOf(target.key)
this.mutableHistoryKeys.value = targetIndex >= 0
? history.slice(0, targetIndex + 1)
: [...history.slice(0, -1), target.key]
return
}
const existingIndex = history.lastIndexOf(target.key)
@@ -631,6 +672,13 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
this.touchEntries()
}
private async prepareMountedView() {
await nextTick()
if (typeof window === 'undefined') return
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
}
private animateProgress(target: number, initialVelocity: number) {
const transaction = this.mutableTransaction.value
if (!transaction) return Promise.resolve()