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:
@@ -2,7 +2,7 @@
|
|||||||
/** @type {import('./$types').LayoutData} */
|
/** @type {import('./$types').LayoutData} */
|
||||||
// /** @type {import('./$types').LayoutProps} */
|
// /** @type {import('./$types').LayoutProps} */
|
||||||
|
|
||||||
let log_lvl: number = 1;
|
let log_lvl: number = 0;
|
||||||
|
|
||||||
// *** Import Svelte specific
|
// *** Import Svelte specific
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
@@ -71,6 +71,20 @@
|
|||||||
|
|
||||||
let last_reload_time = 0;
|
let last_reload_time = 0;
|
||||||
|
|
||||||
|
// Shallow equality guard — avoids triggering Svelte store updates when the merged
|
||||||
|
// object is functionally identical to the current one. Comparing JSON.stringify on
|
||||||
|
// large objects like $ae_loc (site config, device info, flags) is expensive and
|
||||||
|
// runs on every navigation. Key-by-key identity check is O(n keys), not O(n chars).
|
||||||
|
function shallow_equal(a: Record<string, any>, b: Record<string, any>): boolean {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. CONSOLIDATED SYNC EFFECT (One single point of entry for store updates)
|
// 1. CONSOLIDATED SYNC EFFECT (One single point of entry for store updates)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser || !ae_acct) return;
|
if (!browser || !ae_acct) return;
|
||||||
@@ -83,7 +97,7 @@
|
|||||||
const new_api = { ...current_api, ...(ae_acct.api || {}) };
|
const new_api = { ...current_api, ...(ae_acct.api || {}) };
|
||||||
// Deep check for JWT specifically to avoid extra triggers
|
// Deep check for JWT specifically to avoid extra triggers
|
||||||
if (current_api.jwt !== $ae_loc.jwt) new_api.jwt = $ae_loc.jwt;
|
if (current_api.jwt !== $ae_loc.jwt) new_api.jwt = $ae_loc.jwt;
|
||||||
if (JSON.stringify(current_api) !== JSON.stringify(new_api)) {
|
if (!shallow_equal(current_api, new_api)) {
|
||||||
$ae_api = new_api;
|
$ae_api = new_api;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,13 +107,13 @@
|
|||||||
if (!new_loc.sys_menu) new_loc.sys_menu = { hide: false, hide_access_type: false, expand_access_type: false, hide_edit_mode: false, expand_edit_mode: false, hide_user: false, expand_user: false, hide_theme: false, expand_theme: false, hide_cfg: false, expand_cfg: false };
|
if (!new_loc.sys_menu) new_loc.sys_menu = { hide: false, hide_access_type: false, expand_access_type: false, hide_edit_mode: false, expand_edit_mode: false, hide_user: false, expand_user: false, hide_theme: false, expand_theme: false, hide_cfg: false, expand_cfg: false };
|
||||||
if (!new_loc.debug_menu) new_loc.debug_menu = { hide: false, expand: false };
|
if (!new_loc.debug_menu) new_loc.debug_menu = { hide: false, expand: false };
|
||||||
|
|
||||||
if (JSON.stringify(current_loc) !== JSON.stringify(new_loc)) {
|
if (!shallow_equal(current_loc, new_loc)) {
|
||||||
$ae_loc = new_loc;
|
$ae_loc = new_loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const current_slct = $slct;
|
const current_slct = $slct;
|
||||||
const new_slct = { ...current_slct, ...(ae_acct.slct || {}) };
|
const new_slct = { ...current_slct, ...(ae_acct.slct || {}) };
|
||||||
if (JSON.stringify(current_slct) !== JSON.stringify(new_slct)) {
|
if (!shallow_equal(current_slct, new_slct)) {
|
||||||
$slct = new_slct;
|
$slct = new_slct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let log_lvl: number = $state(1);
|
let log_lvl: number = $state(0);
|
||||||
interface Props {
|
interface Props {
|
||||||
/** @type {import('./$types').LayoutData} */
|
/** @type {import('./$types').LayoutData} */
|
||||||
data: any;
|
data: any;
|
||||||
@@ -123,11 +123,22 @@
|
|||||||
if (ae_acct) {
|
if (ae_acct) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
const new_location_obj_li = ae_acct.slct.event_location_obj_li ?? [''];
|
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;
|
$events_slct.event_location_obj_li = new_location_obj_li;
|
||||||
}
|
}
|
||||||
|
|
||||||
const new_id_li__event_location = ae_acct.slct.id_li__event_location ?? [''];
|
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;
|
$events_slct.id_li__event_location = new_id_li__event_location;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -141,33 +152,14 @@
|
|||||||
const id = $events_slct?.event_id;
|
const id = $events_slct?.event_id;
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
if (log_lvl > 1) console.log(`lq__event_obj: event_id = ${id}`);
|
if (log_lvl > 1) console.log(`lq__event_obj: event_id = ${id}`);
|
||||||
let results = await db_events.event.get(id);
|
return 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event Device
|
// Event Device
|
||||||
let lq__event_device_obj = liveQuery(async () => {
|
let lq__event_device_obj = liveQuery(async () => {
|
||||||
const id = $events_slct.event_device_id;
|
const id = $events_slct.event_device_id;
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
let results = await db_events.device.get(id);
|
return 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event File - For Event
|
// Event File - For Event
|
||||||
@@ -220,23 +212,12 @@
|
|||||||
return liveQuery(async () => {
|
return liveQuery(async () => {
|
||||||
if (!id) return [];
|
if (!id) return [];
|
||||||
if (log_lvl > 1)
|
if (log_lvl > 1)
|
||||||
console.log(
|
console.log(`LQ - Event Session list location_id: ${id}`);
|
||||||
`LQ - Event Session list location_id: ${id}`
|
|
||||||
);
|
|
||||||
// Note: .reverse() before .sortBy() is a no-op — sortBy always re-sorts.
|
// 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')
|
.where('event_location_id')
|
||||||
.equals(id)
|
.equals(id)
|
||||||
.sortBy('name');
|
.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))
|
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_conn = $state(false);
|
||||||
let trigger_handle_ws_recv = $state(false);
|
let trigger_handle_ws_recv = $state(false);
|
||||||
let trigger_handle_ws_sent = $state(false);
|
let trigger_handle_ws_sent = $state(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user