From 3ec9aca798ab6f5924bd69b48a7248b6ff0b6280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 22:42:14 +0200 Subject: [PATCH] perf(dashboard): memoise the band math and the module-list parse The dashboard recomputed the client band fallback on every render even when the server snapshot supplied bands, and the module-list-prefs hook reparsed its stored JSON into a fresh object each render, defeating downstream memoisation. Wrap both in useMemo keyed on their real inputs. --- src/app/page.tsx | 19 ++++++++++++++----- src/lib/module-list-prefs.ts | 8 ++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4f267de25..d021b42b3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -532,11 +532,20 @@ export default function DashboardPage() { // — it is pure + cheap — so a still-loading snapshot frame keeps real // bands rather than blank charts; `serverBands ?? clientBands` then // prefers the authoritative server numbers the moment they arrive. - const clientBands = buildDashboardBands({ - dateOfBirth: user?.dateOfBirth ? new Date(user.dateOfBirth) : null, - gender: (user?.gender as "MALE" | "FEMALE" | null | undefined) ?? null, - heightCm: user?.heightCm ?? null, - }); + // Inputs only change with profile facts, so memoise the band math — it + // ran on every dashboard render (and got discarded by `serverBands ??` + // in the snapshot steady state) before this. Keyed on the `user` object + // identity so the React Compiler can preserve the memo (a narrower + // property list trips `preserve-manual-memoization`). + const clientBands = useMemo( + () => + buildDashboardBands({ + dateOfBirth: user?.dateOfBirth ? new Date(user.dateOfBirth) : null, + gender: (user?.gender as "MALE" | "FEMALE" | null | undefined) ?? null, + heightCm: user?.heightCm ?? null, + }), + [user], + ); const bands = serverBands ?? clientBands; const bpTargets = bands.bpTargets; const weightRange = bands.weightRange; diff --git a/src/lib/module-list-prefs.ts b/src/lib/module-list-prefs.ts index c23a396ce..0b2e5f4a7 100644 --- a/src/lib/module-list-prefs.ts +++ b/src/lib/module-list-prefs.ts @@ -19,7 +19,7 @@ * block (the server default order is preserved for the tail). */ -import { useCallback, useSyncExternalStore } from "react"; +import { useCallback, useMemo, useSyncExternalStore } from "react"; export const MODULE_LIST_VIEWS = ["cards", "list"] as const; export type ModuleListView = (typeof MODULE_LIST_VIEWS)[number]; @@ -150,7 +150,11 @@ export function useModuleListPrefs(module: ModuleListKey): { }, [key]); const raw = useSyncExternalStore(subscribe, getSnapshot, () => null); - const prefs = parseModuleListPrefs(raw); + // `useSyncExternalStore` returns a stable `raw` string when storage hasn't + // changed, so keying the parse on it gives downstream `applyOrder` + // memoisation a stable `prefs` reference instead of a fresh object + a + // JSON.parse on every render. + const prefs = useMemo(() => parseModuleListPrefs(raw), [raw]); const write = useCallback( (next: ModuleListPrefs) => {