4.9 KiB
Issue 005: ReplaySourcesPanel Repeated DOM Commits
Summary
ReplaySourcesPanel appears to re-commit many input/checkbox attributes repeatedly on /,
especially in mobile portrait where the panel is always mounted near the top of the page.
This may be a component-local issue, or it may be caused by broader context/session updates. Tackle it after issues 001 and 002 unless profiling still shows it as large.
Severity
Medium.
It is visible in mutation probes, but likely partly downstream of broader invalidation.
Affected Files
webui/src/components/ReplaySourcesPanel/index.jsxwebui/src/App.jsxwebui/src/context/SessionContext.jsxwebui/src/settings/SettingsProvider.jsx
Evidence
Artifact:
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
In an 18s / audit at mobile viewport with 6x CPU throttle, top mutation targets included:
input.accent-emerald-400 name: 5300
input.accent-emerald-400 type: 2650
input#replay-sources-mobile-portrait-title name: 666
input#replay-sources-mobile-portrait-title type: 333
The accent-emerald-400 class maps to ReplaySourcesPanel checkboxes:
<input
type="checkbox"
checked={includeSidebar}
...
className="accent-emerald-400"
/>
and source item checkboxes:
<input
type="checkbox"
checked={selected.includes(item.key)}
onChange={() => onToggle(item.key)}
className="accent-emerald-400"
/>
The mobile portrait route mounts it here:
<ReplaySourcesPanel panelId="replay-sources-mobile-portrait" />
in App.jsx.
Why This Matters
Repeated DOM attribute commits are a symptom of repeated React commits. Even if the attribute values do not change semantically, React still touches DOM attributes when the subtree rerenders/commits.
The panel is not central to driving, so it should not be doing meaningful work during normal control-page idle.
Likely Causes
The component subscribes to several session slices:
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
const mode = useSessionSelector((state) => state.session?.mode || null);
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const replayState = useSessionSelector((state) => state.session?.replay || null);
const latestReplay = useSessionSelector((state) => state.latestReplay);
Potential problems:
session:syncmay provide new array/object identities even when contents are unchanged.normalizeSources(replaySources || [])runs every render and produces new objects.GroupListis not memoized.selected.includes(item.key)is recomputed for each item on each render.remainingMsinterval updates every 250ms when cooldown is active.- Parent/mobile layout re-renders can also re-render this panel.
Fix Strategies
Option A: Memoize Normalized Sources
const sources = useMemo(
() => normalizeSources(replaySources || []),
[replaySources],
);
This is already not memoized in the current code.
Option B: Use Shallow Equality Selectors
For arrays like replaySources and roster, use a selector/equality function that avoids
rerendering when content did not change.
Example:
const replaySources = useSessionSelector(
(state) => state.session?.replaySources ?? [],
shallowReplaySourcesEqual,
);
Option C: Memoize GroupList
const GroupList = React.memo(function GroupList(...) { ... });
Also memoize:
const selectedSet = useMemo(() => new Set(selected), [selected]);
Then use selectedSet.has(item.key).
Option D: Do Not Mount Replay Panel By Default on Mobile
If replay is not a core mobile driving workflow, consider putting it behind a tab/accordion or lazy mounting it only when expanded.
This is a product decision.
Option E: Stop Passing Unstable Callbacks
Memoize callbacks:
const toggleKey = useCallback((key) => { ... }, []);
const handleReplay = useCallback(async () => { ... }, [...]);
This helps if child components are memoized.
Recommended Path
- Fix issues 001 and 002 first.
- Rerun root runtime audit.
- If
ReplaySourcesPanelstill dominates mutation targets:- memoize
sources - memoize
GroupList - add shallow equality selectors
- consider lazy mounting on mobile
- memoize
Validation
Run:
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
Expected improvement:
input.accent-emerald-400 name/type mutations should drop sharply
replay title input name/type mutations should drop
Risks
- Replay source selections and saved panel settings must still update correctly.
- If lazy mounting, preserve persisted title/sidebar settings.
- Do not break replay cooldown display.