[Perf] Fix liveQuery reactivity, silence debug logs, add performance guidelines
- launcher/+layout.svelte: convert lq__event_session_obj from $derived to $derived.by() so Svelte tracks event_session_id as a dependency; the old pattern read the store inside the Dexie async callback where Svelte's tracking is off, so the liveQuery never updated on session change - ae_events__event_file.ts: fix hardcoded log_lvl: 2 in SWR fire-and-forget background refresh (always-on debug logging on every cache hit) → 0 - e_app_sign_in_out.svelte: lower 6 call-site log levels (1×log_lvl:2, 5×log_lvl:1) to 0; sign-in runs on every page load - element_manage_hosted_file_li.svelte: log_lvl:2 → 0 in refresh call; remove log_lvl=1 assignment + debug block inside click handler; log_lvl:1 → 0 in delete call - AE__Performance_Guidelines.md: add 5 Svelte 5 runes rules covering $derived.by() for reactive liveQuery, liveQuery purity, cheap equality guards ($id+updated_on, ID-join, shallow_equal), untrack() requirement, and log_lvl discipline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ export async function load({ params, parent }) {
|
||||
const event_id = params.event_id;
|
||||
|
||||
if (browser) {
|
||||
// GOOD: Fire and forget.
|
||||
// GOOD: Fire and forget.
|
||||
// This function updates IndexedDB in the background.
|
||||
events_func.load_ae_obj_id__event({
|
||||
event_id: event_id,
|
||||
@@ -83,3 +83,158 @@ 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
|
||||
<!-- ❌ WRONG: $events_slct.event_session_id is read inside the async callback.
|
||||
Svelte never tracks it. The liveQuery is created once and never recreates
|
||||
when event_session_id changes. -->
|
||||
let lq__session = $derived(
|
||||
liveQuery(() => db_events.session.get($events_slct.event_session_id))
|
||||
);
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- ✅ CORRECT: $derived.by() captures the ID in the outer synchronous closure.
|
||||
Svelte tracks it. When event_session_id changes, $derived.by() re-runs,
|
||||
creating a new liveQuery with the updated ID. -->
|
||||
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
|
||||
<!-- ❌ WRONG: Store side-effect inside liveQuery async callback. -->
|
||||
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
|
||||
<!-- ✅ CORRECT: liveQuery is pure data-only. Store sync happens in a $effect. -->
|
||||
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
|
||||
<!-- ❌ WRONG: Reading $store.obj inside $effect creates a dependency loop. -->
|
||||
$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
|
||||
<!-- ✅ CORRECT: untrack() reads current store values without registering them
|
||||
as reactive dependencies of the $effect. -->
|
||||
$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.
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function load_ae_obj_li__event_file({
|
||||
offset,
|
||||
order_by_li,
|
||||
try_cache,
|
||||
log_lvl: 2
|
||||
log_lvl: 0
|
||||
});
|
||||
return cached_li;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
account_id: $slct.account_id,
|
||||
user_id: user_id,
|
||||
base_url: $ae_loc.base_url,
|
||||
log_lvl: 2
|
||||
log_lvl: 0
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
account_id: $slct.account_id,
|
||||
null_account_id: false,
|
||||
email: email,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
})
|
||||
.then((user_response) => {
|
||||
if (user_response?.user_id_random) {
|
||||
@@ -291,7 +291,7 @@
|
||||
account_id: $slct.account_id,
|
||||
null_account_id: false,
|
||||
email: user_email,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
if (!ae_promises.load__user_obj_li) {
|
||||
@@ -465,7 +465,7 @@
|
||||
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
|
||||
user_id: $ae_sess.auth__entered_user_id,
|
||||
user_auth_key: $ae_sess.auth__entered_user_key,
|
||||
log_lvl: 2
|
||||
log_lvl: 0
|
||||
})
|
||||
.then((user_response) => {
|
||||
// console.log(`HERE:`, user_response);
|
||||
@@ -520,7 +520,7 @@
|
||||
hidden: 'all',
|
||||
// params_json: params_json,
|
||||
// params: params,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
})
|
||||
.then((person_response) => {
|
||||
// Safety Check: Ensure the response is valid and contains at least one record before accessing index 0.
|
||||
@@ -562,7 +562,7 @@
|
||||
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
|
||||
username: $ae_sess.auth__entered_username,
|
||||
password: $ae_sess.auth__entered_password,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
})
|
||||
.then((user_response) => {
|
||||
if (user_response?.user_id) {
|
||||
@@ -616,7 +616,7 @@
|
||||
hidden: 'all',
|
||||
// params_json: params_json,
|
||||
// params: params,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
})
|
||||
.then((person_response) => {
|
||||
// Safety Check: Ensure the response is valid and contains at least one record before accessing index 0.
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
limit: 250,
|
||||
// params: params,
|
||||
try_cache: true,
|
||||
log_lvl: 2
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
// ae_tmp.show__file_li = false;
|
||||
@@ -158,10 +158,6 @@
|
||||
slct_hosted_file_id = hosted_file_obj.hosted_file_id;
|
||||
slct_hosted_file_obj = hosted_file_obj;
|
||||
}
|
||||
log_lvl = 1;
|
||||
if (log_lvl) {
|
||||
console.log(`slct_hosted_file_kv:`, slct_hosted_file_kv);
|
||||
}
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||
title="Add/Remove file to/from the locally stored uploaded file list. This is referenced by other AE components."
|
||||
@@ -195,7 +191,7 @@
|
||||
link_to_id: link_to_id,
|
||||
rm_orphan: true,
|
||||
fake_delete: false,
|
||||
log_lvl: 1
|
||||
log_lvl: 0
|
||||
});
|
||||
}}
|
||||
class:hidden={!$ae_loc.administrator_access}
|
||||
|
||||
@@ -222,9 +222,12 @@
|
||||
});
|
||||
|
||||
// Event Session (Main View Trigger - Needed for Global Header/Idle)
|
||||
let lq__event_session_obj = $derived(
|
||||
liveQuery(() => db_events.session.get($events_slct.event_session_id))
|
||||
);
|
||||
// $derived.by: capture ID in outer closure so Svelte tracks it as a dependency.
|
||||
// liveQuery callback runs in Dexie's async context where Svelte tracking is off.
|
||||
let lq__event_session_obj = $derived.by(() => {
|
||||
const id = $events_slct.event_session_id;
|
||||
return liveQuery(() => db_events.session.get(id));
|
||||
});
|
||||
|
||||
// Store sync effects — keep liveQuery closures pure (data-only) and sync to
|
||||
// $events_slct here in reactive effects instead. Comparing updated_on + id is
|
||||
|
||||
Reference in New Issue
Block a user