Improve caching
This commit is contained in:
@@ -32,7 +32,7 @@ interface NativeRouteOptions {
|
||||
siblingGroup?: string
|
||||
siblingOrder?: number
|
||||
siblingHistory?: 'push' | 'replace'
|
||||
cache?: boolean
|
||||
cache?: boolean | 'pin'
|
||||
gesture?: boolean | 'edge' | 'full'
|
||||
}
|
||||
```
|
||||
@@ -57,4 +57,29 @@ Applications can call `beginInteractive()`, `updateInteractive()`, and `finishIn
|
||||
|
||||
## Cache semantics
|
||||
|
||||
The active route and recent inactive routes remain mounted. The default limit is eight inactive views per runtime. Older entries keep their route descriptor but are unmounted and lazily restored when revisited. Application data that must survive eviction belongs in an application store.
|
||||
The cache is lazy: application startup mounts the current route, not every sibling. A replace-style sibling is created when it is first visited or previewed and can then remain mounted without becoming a browser-back entry. Recent history targets can also stay warm so predictive Back restores component-local state such as a scrolled list immediately.
|
||||
|
||||
The default limit is four inactive views per runtime. `cache: false` always unmounts an inactive route, while `cache: 'pin'` exempts it from ordinary LRU and manual trimming. A pushed detail route that is popped or dismissed is unmounted after its exit animation unless it is explicitly pinned. If a guard rejects a cached destination, that component tree is evicted because it is no longer a valid navigation target. Older entries keep lightweight route descriptors and are lazily reconstructed if history reaches them again.
|
||||
|
||||
This is deliberately not implemented with a single Vue `<KeepAlive>`. An interactive transition must render the current and destination route instances concurrently, while one `<KeepAlive>` outlet normally activates one selected child. Separate temporary wrappers would themselves be removed and lose their caches. The runtime therefore owns the small multi-view cache and exposes equivalent route-aware lifecycle signals:
|
||||
|
||||
```ts
|
||||
import {
|
||||
onNativeViewActivate,
|
||||
onNativeViewDeactivate,
|
||||
onNativeViewEvict,
|
||||
useNativeViewActiveEffect,
|
||||
useNativeViewLifecycle,
|
||||
} from '@native-vue-router/core'
|
||||
|
||||
const view = useNativeViewLifecycle()
|
||||
|
||||
useNativeViewActiveEffect(() => {
|
||||
const timer = startPolling()
|
||||
return () => stopPolling(timer)
|
||||
})
|
||||
|
||||
onNativeViewEvict((reason) => saveDraft(view.route.value, reason))
|
||||
```
|
||||
|
||||
`isActive` means the route is authoritative. `isVisible` also includes either side of an in-progress transition. Use `useNativeViewActiveEffect` for polling and other work that should pause in a cached tab, or `useNativeViewVisibleEffect` for work needed during the animation. Application data that must survive eviction belongs in an application store.
|
||||
|
||||
@@ -142,11 +142,13 @@ Keeping a tab mounted is a rendering concern; deciding whether Back should visit
|
||||
|
||||
### Approach
|
||||
|
||||
The runtime separates the history-key ledger from the mounted-view cache. A replaced sibling can remain reusable without entering the back path. Statuses distinguish active, inactive, preview, and evicted entries.
|
||||
The runtime separates the history-key ledger from the mounted-view cache. A replaced sibling is created lazily and can remain reusable without entering the back path. Popped pushed routes and guard-rejected destinations are evicted; history descriptors remain available for reconstruction. Statuses distinguish active, inactive, preview, and evicted entries. Route-aware active/visible effects give cached components a way to suspend work.
|
||||
|
||||
### Trade-off
|
||||
|
||||
Mounted routes consume memory. The inactive cache is bounded, and evicted component-local state is not guaranteed to survive. Durable state belongs in Pinia, another store, IndexedDB, or the backend.
|
||||
Mounted routes consume memory. The inactive cache is bounded and can be trimmed by the host, while `cache: false` and `cache: 'pin'` make exceptional route policy explicit. Evicted component-local state is not guaranteed to survive. Durable state belongs in Pinia, another store, IndexedDB, or the backend.
|
||||
|
||||
Vue's `<KeepAlive>` was not used as the cache owner. One shared wrapper is designed to select a current child, but a predictive gesture renders two route instances concurrently. Creating independent wrappers per temporary route layer would make wrapper lifetime control cache lifetime and complicate deterministic LRU eviction. The trade-off is a small router-owned cache with lifecycle APIs instead of Vue's built-in activated/deactivated hooks.
|
||||
|
||||
## Challenge 11: accessibility with concurrent routes
|
||||
|
||||
|
||||
@@ -92,7 +92,9 @@ Pointer capture keeps delivery stable after recognition. Recognizer state is det
|
||||
|
||||
Primary sibling routes normally replace one another in history. Their component trees can still remain mounted in the view cache. This means returning to a tab can preserve local UI state without making every tab selection a browser-back destination.
|
||||
|
||||
The cache has a configurable inactive-view limit. Older entries retain route descriptors but their component trees are unmounted and restored lazily. Durable application data should live in an application store rather than depending on a route component remaining cached forever.
|
||||
Siblings are mounted lazily on their first visit or gesture preview, not all at application startup. The cache has a configurable inactive-view limit and an explicit opt-out/pin policy. Popping a pushed route releases its component tree after the exit animation; a rejected cached guard target is evicted immediately. Older entries retain route descriptors but their component trees are unmounted and restored lazily. Durable application data should live in an application store rather than depending on a route component remaining cached forever.
|
||||
|
||||
A normal Vue `<KeepAlive>` is excellent when one outlet selects one child. It is not the cache primitive here because an interactive transition needs two independently addressed route instances to be active at once. Instead, the router owns those sibling view instances and exposes active, visible, cached, and eviction lifecycle signals. This preserves the useful KeepAlive distinction—mounted versus currently active—without coupling navigation history to Vue's single-child activation model.
|
||||
|
||||
## Platform behavior
|
||||
|
||||
@@ -122,4 +124,3 @@ Native Vue Router addresses this by treating interactive navigation as its own s
|
||||
## Scope
|
||||
|
||||
The library is a client-side navigation runtime, not a replacement for Vue Router and not a native rendering engine. It can closely reproduce native navigation composition and input behavior, but final fidelity still depends on application design, frame performance, platform embedding, typography, safe-area handling, and avoiding expensive work in route components during a gesture.
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ Mobile operating systems can reserve gestures before web content receives them.
|
||||
|
||||
## Electron
|
||||
|
||||
Call `disableElectronHistoryGestures(app.commandLine)` before `app.whenReady()`. It disables Chromium's `OverscrollHistoryNavigation`, preventing the host from racing the renderer's interactive stack. The included preload bridge maps app commands and Alt+Arrow shortcuts into the renderer adapter without enabling Node integration.
|
||||
Call `disableElectronHistoryGestures(app.commandLine)` before `app.whenReady()`. It disables Chromium's `OverscrollHistoryNavigation`, preventing the host from racing the renderer's interactive stack. The included preload bridge maps app commands, memory-pressure notifications, and Alt+Arrow shortcuts into the renderer adapter without enabling Node integration.
|
||||
|
||||
The demo switches to hash history under `file:` so packaged deep navigation never asks the filesystem for route paths.
|
||||
|
||||
## Capacitor
|
||||
|
||||
`createCapacitorAdapter()` handles Android hardware back, Universal/App Links, launch URLs, pause cancellation, root exit, and native haptic feedback.
|
||||
`createCapacitorAdapter()` handles Android hardware back, Universal/App Links, launch URLs, pause cancellation, root exit, and native haptic feedback. It trims inactive views when the native app pauses by default; set `trimCacheOnPause: false` only when the application deliberately prefers warm views over background memory release.
|
||||
|
||||
The checked-in iOS and Android projects use Capacitor 8 and include App, Haptics, Splash Screen, and Status Bar plugins. Rebuild the web bundle before `npx cap sync`.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ The runtime is intended to scale in four different ways: number of routes, sessi
|
||||
|
||||
### Route and DOM scale
|
||||
|
||||
Only the active route, recent inactive routes, and a transaction preview need mounted component trees. `maxInactive` bounds the inactive mounted cache; the default is eight. Older views are marked evicted and lazily remounted when needed.
|
||||
Only the active route, previously visited inactive routes, and a transaction preview need mounted component trees. Siblings are not instantiated eagerly at application startup. `maxInactive` bounds the inactive mounted cache; the default is four. Pinned entries are deliberately outside this ordinary budget. Older views are marked evicted and lazily remounted when needed.
|
||||
|
||||
During an interaction, animation work concerns two surfaces regardless of total route count:
|
||||
|
||||
@@ -68,6 +68,8 @@ The current implementation uses linear searches through view entries for some re
|
||||
|
||||
The view cache is not an application data cache. Large collections, message history, drafts, and durable form state should live outside route component instances. This allows view eviction to remain cheap and makes state available whether a route is reached through a gesture, deep link, background notification, or restored session.
|
||||
|
||||
Cached components should also avoid doing active-screen work indefinitely. Route-aware lifecycle effects let polling, animation loops, media, and subscriptions stop while a component is inactive and resume without losing its local render state. Hosts can call `trimCache()` under memory pressure; the Capacitor adapter does so when the app pauses by default.
|
||||
|
||||
Preview loading should fetch only what the destination needs to render its initial surface. Applications can use route-level lazy imports, shared stores, request deduplication, and cancellation to avoid duplicating expensive work during a cancelled preview.
|
||||
|
||||
### Team and feature scale
|
||||
|
||||
Reference in New Issue
Block a user