Perf: replace JSON.stringify comparisons with efficient identity checks

JSON.stringify on large store objects (ae_loc, ae_api, slct, event lists)
was running on every navigation, comparing serialized strings of potentially
deep objects. Replace with targeted comparators:

- Root +layout.svelte: add shallow_equal() helper — O(n keys) key-by-key
  identity check instead of O(serialized bytes). Used for ae_api, ae_loc,
  and slct sync guards.

- Launcher +layout.svelte: ID-list join-compare for event_location_obj_li
  and id_li__event_location (O(n) string vs O(n*m) stringify). Refactor
  liveQuery closures to be pure (data-only, no store reads/writes inside
  the async Dexie context where Svelte reactivity tracking is undefined).
  Move store sync into separate $effects that compare updated_on + id
  (O(1)) or ID-join (O(n)) rather than full object serialization.
This commit is contained in:
Scott Idem
2026-03-10 14:20:57 -04:00
parent 2c0eba4130
commit ffc430a727
2 changed files with 79 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
let log_lvl: number = $state(1);
let log_lvl: number = $state(0);
interface Props {
/** @type {import('./$types').LayoutData} */
data: any;
@@ -123,11 +123,22 @@
if (ae_acct) {
untrack(() => {
const new_location_obj_li = ae_acct.slct.event_location_obj_li ?? [''];
if (JSON.stringify($events_slct.event_location_obj_li) !== JSON.stringify(new_location_obj_li)) {
// Compare by extracting IDs only — object identity (===) won't work for
// plain JS objects from the store. Joining IDs is cheap and avoids a full
// JSON.stringify of potentially large location objects on every navigation.
const current_obj_ids = ($events_slct.event_location_obj_li ?? [])
.map((o: any) => o?.event_location_id ?? o)
.join(',');
const new_obj_ids = new_location_obj_li
.map((o: any) => o?.event_location_id ?? o)
.join(',');
if (current_obj_ids !== new_obj_ids) {
$events_slct.event_location_obj_li = new_location_obj_li;
}
const new_id_li__event_location = ae_acct.slct.id_li__event_location ?? [''];
if (JSON.stringify($events_slct.id_li__event_location) !== JSON.stringify(new_id_li__event_location)) {
// ID list contains plain strings — join-compare is O(n) and avoids JSON.stringify.
if (($events_slct.id_li__event_location ?? []).join(',') !== new_id_li__event_location.join(',')) {
$events_slct.id_li__event_location = new_id_li__event_location;
}
});
@@ -141,33 +152,14 @@
const id = $events_slct?.event_id;
if (!id) return null;
if (log_lvl > 1) console.log(`lq__event_obj: event_id = ${id}`);
let results = await db_events.event.get(id);
if ($events_slct.event_obj && results) {
if (
JSON.stringify($events_slct.event_obj) !==
JSON.stringify(results)
) {
$events_slct.event_obj = { ...results };
}
}
return results;
return await db_events.event.get(id);
});
// Event Device
let lq__event_device_obj = liveQuery(async () => {
const id = $events_slct.event_device_id;
if (!id) return null;
let results = await db_events.device.get(id);
if ($events_slct.event_device_obj && results) {
if (
JSON.stringify($events_slct.event_device_obj) !==
JSON.stringify(results)
) {
$events_slct.event_device_obj = { ...results };
}
}
return results;
return await db_events.device.get(id);
});
// Event File - For Event
@@ -220,23 +212,12 @@
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1)
console.log(
`LQ - Event Session list location_id: ${id}`
);
console.log(`LQ - Event Session list location_id: ${id}`);
// Note: .reverse() before .sortBy() is a no-op — sortBy always re-sorts.
let results = await db_events.session
return await db_events.session
.where('event_location_id')
.equals(id)
.sortBy('name');
if (
$events_slct.event_session_obj_li &&
JSON.stringify($events_slct.event_session_obj_li) !==
JSON.stringify(results)
) {
$events_slct.event_session_obj_li = [...(results || [])];
}
return results;
});
});
@@ -245,6 +226,49 @@
liveQuery(() => db_events.session.get($events_slct.event_session_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
// O(1) vs O(serialized-bytes) for JSON.stringify and avoids running inside a
// Dexie async context where Svelte's reactivity tracking is undefined.
$effect(() => {
const result = $lq__event_obj;
if (result) {
untrack(() => {
if (result.updated_on !== $events_slct.event_obj?.updated_on ||
result.id !== $events_slct.event_obj?.id) {
$events_slct.event_obj = { ...result };
}
});
}
});
$effect(() => {
const result = $lq__event_device_obj;
if (result) {
untrack(() => {
if (result.updated_on !== $events_slct.event_device_obj?.updated_on ||
result.id !== $events_slct.event_device_obj?.id) {
$events_slct.event_device_obj = { ...result };
}
});
}
});
$effect(() => {
const results = $lq__event_session_obj_li;
if (results) {
untrack(() => {
const current = $events_slct.event_session_obj_li ?? [];
// Compare by joining IDs — O(n) string compare vs O(n*m) JSON.stringify.
const new_ids = (results as any[]).map((r: any) => r.id ?? r.event_session_id).join(',');
const cur_ids = current.map((r: any) => r.id ?? r.event_session_id).join(',');
if (new_ids !== cur_ids) {
$events_slct.event_session_obj_li = [...(results as any[])];
}
});
}
});
let trigger_handle_ws_conn = $state(false);
let trigger_handle_ws_recv = $state(false);
let trigger_handle_ws_sent = $state(false);