[Guide] Build a Fitness Dashboard (Profiler Signals Optional) - implementation notes
The Fitness Dashboard guide frames performance as a route posture problem: decide "tick cadence" and remount boundaries in the route contract, then store derived card outputs so the dashboard stays stable even when the underlying data churns. I like the premise that the profiler is optional—what matters is making performance decisions explicit and observable.
How did you decide which cards should remount on identity changes versus preserve local UI state? Did you encode a render tick posture (warm/cool) as route state, or keep it as a local toggle? What memoization stance worked in practice: memo everything, memo nothing, or memo only derived selectors? How did you implement "freshness switches" without making the UI feel like it randomly resets?
Comments (18)
Back to latestBig win for me was treating remount keys as part of the contract.
Once you commit to a remount boundary, performance bugs turn into normal state bugs.
We made the remount boundary explicit on the card shell. It looks like this:
tsx
function CardShell({ athleteId, cardKey, children }: any) {
const identityKey = `${athleteId}:${cardKey}`;
return <section key={identityKey} data-card={cardKey}>{children}</section>;
}
Now when someone says "my chart reset", we can point to the identity key and decide if it's intended.
Counterpoint: too many remount keys make the dashboard feel flaky.
We only remount when the semantics change (athlete swap, date range swap). UI mode toggles should not remount.
Same. "Freshness" isn't a synonym for "reset".
We treated it like: reset only when the user's mental model should reset.
We encoded tick posture in route state because it affects everything (requests, polling, derived summaries).
If it's local, two panels disagree and you can't reason about it.
Memo stance: we memo only the derived selectors and keep components dumb.
If you memo everything, you end up debugging memoization instead of UI.
Stored derived cards were the only way our dashboard stayed coherent across route switches.
Otherwise each card derives slightly differently and you get "why is this number different" arguments.
We rendered evidence for tick posture + remount version so screenshots tell the story:
tsx
<main
data-tick-posture={route.tick}
data-freshness={route.freshness}
data-remount-version={route.remountVersion}
/>
It's not pretty, but it's extremely effective.
Freshness switches: we used a small "confirm" affordance if the switch would reset filters.
Users hate surprise resets even when they're "correct".
Counterpoint: storing derived cards can hide performance problems.
If the derive is expensive and you do it too often, you just moved the cost somewhere else.
True, but I like it because it forces you to choose *when* to pay the cost (on mutation) rather than paying it on every render.
It's also easier to measure because the derive is a named path.
Docs tie-in: the idea of explicit posture matches the perf docs pretty closely.
Related: Performance and Rendering (Best Practices) and Routing and Navigation.
We used a "cool tick" mode that freezes derived cards for 10s windows and it made scrolling smoother.
The key is rendering the tick window as evidence so people don't think the data is stale by accident.
A practical trick: keep chart hover state local and never store it. It's not a contract.
We store only the derived summary numbers so navigation stays stable.
Memoization stance that worked for us: memo only at the boundary of "raw data -> derived card".
Everything after that is render-only and doesn't need cleverness.
Remount boundaries also helped with correctness: we stopped leaking previous athlete state into new views.
Performance aside, it fixed a bunch of "wrong data" bugs.
If you keep tick posture in route state, make sure back/forward replays it correctly.
Otherwise the dashboard feels like it has moods.
The "profiler optional" framing is good for teams. It lowers the bar: you can still do coherent perf work without being an expert.
Explicit contracts + evidence gets you most of the way.
Freshness switches were easiest when we treated them like a mode: switch -> remount version increments -> derived cards recompute once.
Trying to make it incremental led to a lot of partial updates and confusing UI.
I liked that the guide doesn't treat memoization like a superstition.
It's a stance: decide where memo boundaries belong and make them part of the route contract.