188 lines
9.2 KiB
Markdown
188 lines
9.2 KiB
Markdown
---
|
|
phase: 14-performance-optimization
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- frontend/index.ts
|
|
- frontend/index.html
|
|
- frontend/index.css
|
|
autonomous: true
|
|
requirements: [PERF-NAV-01, PERF-NAV-02]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Navigating between views does not destroy and recreate components"
|
|
- "Previously visited views retain their DOM and internal state (scroll position, expanded items)"
|
|
- "Only the active view is visible; inactive views are hidden with display: none"
|
|
- "Navigation between cached views feels instant (no data refetch, no virtualizer reinit)"
|
|
artifacts:
|
|
- path: "frontend/index.ts"
|
|
provides: "View cache manager that creates views once and toggles visibility"
|
|
contains: "display"
|
|
- path: "frontend/index.css"
|
|
provides: "Hidden state for inactive views"
|
|
contains: "display: none"
|
|
key_links:
|
|
- from: "frontend/index.ts"
|
|
to: "#main-content"
|
|
via: "View cache toggling active/hidden children"
|
|
pattern: "display"
|
|
- from: "frontend/index.ts"
|
|
to: "navigate event"
|
|
via: "Show cached view or create new one"
|
|
pattern: "navigate"
|
|
---
|
|
|
|
<objective>
|
|
Replace the innerHTML destruction/recreation navigation pattern with a view caching system that keeps previously visited views in the DOM (hidden) and toggles visibility on navigation.
|
|
|
|
Purpose: Currently, every navigation event destroys the active view via `innerHTML = ''` and creates a new component from scratch. This means virtualizers reinitialize, data refetches from cache, scroll positions must be restored, and cover art images reload. By caching views in the DOM and toggling `display: none` / `display: block`, navigation becomes instant.
|
|
|
|
Output: Navigation between primary views (tracks, albums, artists, genres, playlists, settings) is instant — no component destruction, no virtualizer reinit, no scroll position loss.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
|
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@frontend/index.ts
|
|
@frontend/index.html
|
|
@frontend/index.css
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Key navigation patterns from index.ts -->
|
|
From frontend/index.ts:
|
|
- Navigation via CustomEvent('navigate', { detail: { view: '...' } })
|
|
- Primary views: 'albums', 'tracks', 'playlists', 'artists', 'genres', 'settings'
|
|
- Detail views: 'artist-details', 'playlist-details', 'genre-details' (take extra attributes)
|
|
- mainContent = document.getElementById('main-content')
|
|
- Currently: mainContent.innerHTML = '<component-tag></component-tag>' for each view
|
|
|
|
From frontend/index.html:
|
|
- <main class="main-panel" id="main-content"><track-list></track-list></main>
|
|
- Default view is track-list (rendered in HTML)
|
|
|
|
From frontend/index.css:
|
|
- .main-panel > * { height: 100%; }
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Implement view caching navigation system</name>
|
|
<files>frontend/index.ts, frontend/index.html, frontend/index.css</files>
|
|
<action>
|
|
Replace the `innerHTML`-based navigation with a view cache that keeps views alive in the DOM.
|
|
|
|
**Design:**
|
|
- Maintain a `Map<string, HTMLElement>` called `viewCache` for primary views (tracks, albums, artists, genres, playlists, settings).
|
|
- Detail views (artist-details, playlist-details, genre-details) are NOT cached — they are ephemeral and created fresh each time (because they depend on specific IDs/names that change on each navigation). When navigating to a detail view, remove any existing detail view element and create a fresh one.
|
|
- On navigation, hide the current view (`display: none`), then show the target view. If the target view isn't in the cache yet, create it and add to the cache.
|
|
|
|
**Implementation in index.ts:**
|
|
|
|
1. Add a `viewCache` Map and a `currentView` string variable at module scope:
|
|
```typescript
|
|
const viewCache = new Map<string, HTMLElement>();
|
|
let currentViewEl: HTMLElement | null = null;
|
|
let currentDetailEl: HTMLElement | null = null;
|
|
```
|
|
|
|
2. Define a `VIEW_TAGS` mapping for cacheable primary views:
|
|
```typescript
|
|
const VIEW_TAGS: Record<string, string> = {
|
|
tracks: 'track-list',
|
|
albums: 'cover-grid',
|
|
artists: 'artists-view',
|
|
genres: 'genres-view',
|
|
playlists: 'playlist-view',
|
|
settings: 'config-page',
|
|
};
|
|
```
|
|
|
|
3. Replace the entire `document.addEventListener('navigate', ...)` handler with a new one that:
|
|
|
|
a. For primary views (key exists in VIEW_TAGS):
|
|
- If a detail view element exists, remove it from DOM and set `currentDetailEl = null`
|
|
- If the view is already in `viewCache`, retrieve it; otherwise create it via `document.createElement(VIEW_TAGS[view])` and add to both `viewCache` and `mainContent`
|
|
- Hide `currentViewEl` by setting `style.display = 'none'`
|
|
- Show the target element by setting `style.display = ''` (empty string restores the default)
|
|
- Update `currentViewEl` reference
|
|
|
|
b. For detail views (artist-details, playlist-details, genre-details):
|
|
- Hide `currentViewEl` (set display: none)
|
|
- If `currentDetailEl` exists, remove it from DOM
|
|
- Create the detail element, set attributes (artistId, artistName, etc.), append to `mainContent`
|
|
- Set `currentDetailEl` to the new element
|
|
|
|
4. Initialize the default view (track-list) from the HTML:
|
|
```typescript
|
|
const initialTrackList = mainContent.querySelector('track-list');
|
|
if (initialTrackList) {
|
|
viewCache.set('tracks', initialTrackList as HTMLElement);
|
|
currentViewEl = initialTrackList as HTMLElement;
|
|
}
|
|
```
|
|
|
|
**In index.html:** No changes needed — `<track-list></track-list>` remains the default.
|
|
|
|
**In index.css:** Add a rule for hidden views:
|
|
```css
|
|
.main-panel > [data-view-hidden] {
|
|
display: none !important;
|
|
}
|
|
```
|
|
|
|
Actually, simpler to just use inline `style.display` since we control it in JS. No CSS changes needed for this. BUT we should add `contain: layout style paint` to hidden views to ensure they don't participate in layout even if display:none is somehow bypassed (belt and suspenders — Plan 14-01 already adds containment but this is the navigation-specific insurance).
|
|
|
|
**Important considerations:**
|
|
- The `searchStore.setCurrentView(view)` call must remain at the top of the handler
|
|
- View elements get `connectedCallback` called only once (on first creation), and `disconnectedCallback` is never called (they stay in DOM). This is fine — Lit components handle this correctly. Controllers subscribe in `hostConnected` and unsubscribe in `hostDisconnected`, so subscriptions stay active. This is INTENDED for cached views.
|
|
- Memory consideration: We're keeping at most 6 primary view components alive. Each is a single custom element with its own shadow DOM. The data they hold (tracks, albums, etc.) is already in the store cache regardless. The only extra memory is the DOM nodes for the virtualizer's rendered items (typically ~20-50 visible items per view). This is negligible.
|
|
- Scroll position: With view caching, scroll positions are naturally preserved because the DOM is never destroyed. This means the existing scroll save/restore logic in each component (`visibilityChanged` handlers, `scrollToIndex` calls) can eventually be simplified, but DO NOT remove them in this plan — they still serve as fallback for data invalidation scenarios.
|
|
</action>
|
|
<verify>
|
|
`cd frontend && npx vite build --mode development 2>&1 | tail -5` builds without errors.
|
|
Functional test: Start app → see tracks (default) → click Albums → albums appear → click Tracks → tracks reappear instantly WITH scroll position preserved → click Artists → artists appear → click back to Albums → albums still show previously loaded content → navigate to artist-details → back to Artists → artists view preserved.
|
|
</verify>
|
|
<done>
|
|
- Primary views (tracks, albums, artists, genres, playlists, settings) are created once and cached in DOM
|
|
- Navigation toggles visibility instead of destroying/recreating
|
|
- Detail views (artist-details, playlist-details, genre-details) are still created fresh (they're parameterized)
|
|
- Scroll positions naturally preserved by keeping DOM alive
|
|
- No component destruction means no virtualizer reinit, no data refetch, no image reload
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. Build succeeds
|
|
2. Navigate tracks → albums → tracks: track list shows same scroll position
|
|
3. Navigate to albums → scroll down → navigate tracks → back to albums: scroll position preserved
|
|
4. Navigate to artist-details → back button → artists view: artists view preserved
|
|
5. Navigate to playlist-details → back → playlists: playlists preserved
|
|
6. Library scan completes → views update correctly (data invalidation still triggers refetch)
|
|
7. Search works across all views (search state preserved per view)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- No `innerHTML = '<...>'` patterns remain in index.ts for primary views
|
|
- `viewCache` Map maintains at most 6 cached view elements
|
|
- Navigation between cached views takes <16ms (one frame)
|
|
- All existing navigation paths still work (primary views, detail views, settings)
|
|
- No memory leaks (view count bounded, no listener accumulation)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/14-performance-optimization/14-02-SUMMARY.md`
|
|
</output>
|