Files
yellowjacket/.planning/phases/14-performance-optimization/14-02-PLAN.md
T

9.2 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14-performance-optimization 02 execute 1
frontend/index.ts
frontend/index.html
frontend/index.css
true
PERF-NAV-01
PERF-NAV-02
truths artifacts key_links
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)
path provides contains
frontend/index.ts View cache manager that creates views once and toggles visibility display
path provides contains
frontend/index.css Hidden state for inactive views display: none
from to via pattern
frontend/index.ts #main-content View cache toggling active/hidden children display
from to via pattern
frontend/index.ts navigate event Show cached view or create new one navigate
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.

<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>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @frontend/index.ts @frontend/index.html @frontend/index.css 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 = '' for each view

From frontend/index.html:

  • Default view is track-list (rendered in HTML)

From frontend/index.css:

  • .main-panel > * { height: 100%; }
Task 1: Implement view caching navigation system frontend/index.ts, frontend/index.html, frontend/index.css 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:
const viewCache = new Map<string, HTMLElement>();
let currentViewEl: HTMLElement | null = null;
let currentDetailEl: HTMLElement | null = null;
  1. Define a VIEW_TAGS mapping for cacheable primary views:
const VIEW_TAGS: Record<string, string> = {
    tracks: 'track-list',
    albums: 'cover-grid',
    artists: 'artists-view',
    genres: 'genres-view',
    playlists: 'playlist-view',
    settings: 'config-page',
};
  1. 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
  2. Initialize the default view (track-list) from the HTML:

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:

.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. 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.
  • 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
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)

<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>
After completion, create `.planning/phases/14-performance-optimization/14-02-SUMMARY.md`