# Performance Guidelines: Non-Blocking Load Pattern (SvelteKit + Dexie) ## Overview To ensure instant page transitions and a high-performance feel, the Aether platform utilizes a **Non-Blocking Load Pattern** (also known as Stale-While-Revalidate or SWR). This pattern leverages Dexie's `liveQuery` for reactive UI and SvelteKit's `load` functions for background data synchronization. ## 🚀 The Core Principle **Never block the `load` function with API calls if the data is already being observed by a `liveQuery`.** The page should render *instantly* using cached data from IndexedDB. Fresh data from the API should settle in the background and update the UI automatically via reactivity. --- ## ❌ Anti-Pattern (Blocking) This pattern causes a "white screen" or "frozen UI" while the browser waits for the API response. ```typescript // +page.ts export async function load({ params, parent }) { const data = await parent(); const event_id = params.event_id; // BAD: This blocks the navigation until the API responds. const fresh_data = await events_func.load_ae_obj_id__event({ event_id: event_id, try_cache: true }); return { ...data, event_obj: fresh_data }; } ``` ## ✅ Best Practice (Non-Blocking / SWR) This pattern completes the navigation immediately. ```typescript // +page.ts export async function load({ params, parent }) { const data = await parent(); const event_id = params.event_id; if (browser) { // GOOD: Fire and forget. // This function updates IndexedDB in the background. events_func.load_ae_obj_id__event({ event_id: event_id, try_cache: true }); } return data; // Navigation completes instantly } ``` ```svelte {#if $lq__event_obj}

{$lq__event_obj.name}

{:else}

Loading...

{/if} ``` --- ## 🛠️ When to use Await Use `await` in `load` functions ONLY for: 1. **Critical Auth Checks:** If you must verify a session before even showing a layout. 2. **Parent Data:** `const data = await parent();` is necessary to build the context. 3. **Server-Side Rendering (SSR):** If the data *must* be present in the initial HTML for SEO (rare for Aether feature modules). ## 📈 Performance Gains By adopting this pattern across the Events module, we achieved: - **~200-500ms reduction** in perceived page load time. - **Elimination of waterfalls** (sequential API calls). - **Better offline support**, as the UI is always ready to show what's in the local cache. --- ## Svelte 5 Runes + liveQuery: Critical Patterns These rules apply to all Svelte 5 runes-mode components (the entire Aether frontend). Violations here are a common source of subtle reactivity bugs and unnecessary re-renders. ### Rule 1: Use `$derived.by()` when liveQuery depends on a reactive value **The problem:** `$derived(liveQuery(callback))` looks like it should re-run when a store value inside `callback` changes. It does NOT. Svelte tracks reactive dependencies synchronously during the expression evaluation. The `liveQuery` callback is called later inside Dexie's async context — Svelte's tracking is already finished. The dependency is never registered. ```svelte let lq__session = $derived( liveQuery(() => db_events.session.get($events_slct.event_session_id)) ); ``` ```svelte let lq__session = $derived.by(() => { const id = $events_slct.event_session_id; // tracked here, synchronously return liveQuery(() => db_events.session.get(id)); }); ``` **Rule of thumb:** If the liveQuery result changes based on a reactive value (store property, `$state`, `$props`), always use `$derived.by()`. Reserve `$derived(liveQuery(...))` only for liveQueries that watch a table broadly and don't filter by a reactive value. --- ### Rule 2: Keep liveQuery closures pure (data-only) **The problem:** Writing to a Svelte store inside a liveQuery callback runs inside Dexie's async transaction context. Svelte's reactive tracking is undefined there. The write may fire at unpredictable times and create hard-to-debug reactivity loops. ```svelte let lq__event_obj = liveQuery(async () => { const obj = await db_events.event.get($events_slct.event_id); if (obj) $events_slct.event_obj = obj; // BAD: side-effect in async context return obj; }); ``` ```svelte let lq__event_obj = liveQuery(async () => { const id = $events_slct.event_id; if (!id) return null; return await db_events.event.get(id); }); $effect(() => { const result = $lq__event_obj; if (result) { untrack(() => { // Cheap equality guard — only write if something actually changed. if (result.updated_on !== $events_slct.event_obj?.updated_on || result.id !== $events_slct.event_obj?.id) { $events_slct.event_obj = { ...result }; } }); } }); ``` --- ### Rule 3: Use cheap equality guards in `$effect` before writing to stores Every store write in a `$effect` triggers downstream reactivity. Always guard with a comparison before writing. The cost of the comparison is always less than the cost of spurious re-renders. **For single objects** — compare `id` + `updated_on` (O(1)): ```typescript if (result.id !== $store.obj?.id || result.updated_on !== $store.obj?.updated_on) { $store.obj = { ...result }; } ``` **For arrays** — join IDs into a string (O(n)), not `JSON.stringify` (O(n × field_count)): ```typescript const new_ids = results.map(r => r.id).join(','); const cur_ids = ($store.list ?? []).map(r => r.id).join(','); if (new_ids !== cur_ids) { $store.list = [...results]; } ``` **For flat objects** (e.g., merged config) — shallow key-by-key comparison (O(n keys)): ```typescript function shallow_equal(a, b) { const keys_a = Object.keys(a); const keys_b = Object.keys(b); if (keys_a.length !== keys_b.length) return false; for (const k of keys_a) { if (a[k] !== b[k]) return false; } return true; } if (!shallow_equal(current, new_val)) { $store = new_val; } ``` **Never use `JSON.stringify` for equality.** It serializes the full object tree on every reactive cycle and is O(total serialized bytes). --- ### Rule 4: Always use `untrack()` when writing to stores inside `$effect` Without `untrack()`, reading a store to check its current value inside `$effect` registers it as a dependency — the effect re-runs whenever it writes, creating an infinite loop. ```svelte $effect(() => { const result = $lq__obj; if (result.id !== $store.obj?.id) { // Reading $store.obj here is a dependency! $store.obj = result; // This write re-triggers the effect. } }); ``` ```svelte $effect(() => { const result = $lq__obj; // Tracked: effect re-runs when liveQuery emits if (result) { untrack(() => { // Not tracked: reading $store.obj here won't cause a re-run. if (result.id !== $store.obj?.id) { $store.obj = result; } }); } }); ``` --- ### Rule 5: Guard `console.log` calls with `log_lvl` Raw `console.log(obj)` eagerly serializes objects (even large ones) on every call, blocking the main thread. All debug logging must be guarded. ```typescript let log_lvl: number = $state(0); // Set to 0 in production; raise locally to debug. // ❌ WRONG: Always runs, always serializes. console.log('Result:', result_obj); // ✅ CORRECT: Zero-cost when log_lvl is 0. if (log_lvl) console.log('Result:', result_obj); if (log_lvl > 1) console.log('Verbose:', result_obj); // Extra-verbose tier ``` **Never hardcode `log_lvl: 2` in a call-site or override `log_lvl` inside a function body.** The parameter default exists so callers can control verbosity. Overriding it forces debug logging regardless of what the caller passed.