perf(tracks): bound playback rendering work

This commit is contained in:
lingniu
2026-07-16 04:06:08 +08:00
parent 982acb7730
commit 8e8f3f9f5c
7 changed files with 143 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { TrackPlaybackResponse } from '../../api/types';
import { formatDuration, sampledEventIndex, trackCsv } from './track';
import { buildTrackRailSelection, formatDuration, sampledEventIndex, trackCsv, trackPlaybackInterval } from './track';
describe('track domain', () => {
it('formats durations and maps original event indices to sampled points', () => {
@@ -9,6 +9,26 @@ describe('track domain', () => {
expect(sampledEventIndex({ index: 50, sampledIndex: 7 } as never, 11, 101)).toBe(7);
});
it('precomputes sparse rail highlights and bounds the playback cadence', () => {
const playback = {
points: Array.from({ length: 8 }, (_, index) => ({ longitude: index, latitude: index })),
stops: [{ sampledIndex: 3 }],
events: [{ index: 50 }, { index: 75, sampledIndex: 6 }],
summary: { pointCount: 101 }
} as TrackPlaybackResponse;
const selection = buildTrackRailSelection(playback);
expect(selection.stopIndexesByPoint[1]).toBeUndefined();
expect(selection.stopIndexesByPoint[2]).toEqual([0]);
expect(selection.stopIndexesByPoint[3]).toEqual([0]);
expect(selection.stopIndexesByPoint[4]).toEqual([0]);
expect(selection.eventIndexesByPoint[4]).toEqual([0]);
expect(selection.eventIndexesByPoint[6]).toEqual([1]);
expect(trackPlaybackInterval(1)).toBe(300);
expect(trackPlaybackInterval(4)).toBe(75);
expect(trackPlaybackInterval(100)).toBe(55);
});
it('exports the current result with UTF-8 BOM and escaped values', () => {
const track = {
plate: '粤A,001', vin: 'VIN', summary: { startTime: '2026-07-03 10:00:00' },

View File

@@ -16,6 +16,30 @@ export function sampledEventIndex(event: TrackPlaybackEvent, sampledCount: numbe
return Math.max(0, Math.min(sampledCount - 1, Math.round(event.index * (sampledCount - 1) / (originalCount - 1))));
}
export function trackPlaybackInterval(speed: number) {
return Math.max(55, 300 / Math.max(0.5, speed));
}
export function buildTrackRailSelection(track?: TrackPlaybackResponse) {
const pointCount = track?.points.length ?? 0;
const stopIndexesByPoint: Array<number[] | undefined> = new Array(pointCount);
const eventIndexesByPoint: Array<number[] | undefined> = new Array(pointCount);
if (!track || pointCount === 0) return { stopIndexesByPoint, eventIndexesByPoint };
track.stops.forEach((stop, stopIndex) => {
for (let offset = -1; offset <= 1; offset += 1) {
const pointIndex = stop.sampledIndex + offset;
if (pointIndex < 0 || pointIndex >= pointCount) continue;
(stopIndexesByPoint[pointIndex] ??= []).push(stopIndex);
}
});
track.events.forEach((event, eventIndex) => {
const pointIndex = sampledEventIndex(event, pointCount, track.summary.pointCount);
(eventIndexesByPoint[pointIndex] ??= []).push(eventIndex);
});
return { stopIndexesByPoint, eventIndexesByPoint };
}
function csvCell(value: unknown) {
const text = String(value ?? '');
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;

View File

@@ -9,8 +9,11 @@ const overlaySetMap = vi.fn();
const mapOff = vi.fn();
const mapDestroy = vi.fn();
const markerListeners: Array<{ overlay: TestOverlay; event: string; handler: () => void }> = [];
const overlayInstances: TestOverlay[] = [];
const mapInstances: TestMap[] = [];
class TestOverlay {
constructor() { overlayInstances.push(this); }
on = vi.fn((event: string, handler: () => void) => markerListeners.push({ overlay: this, event, handler }));
off = overlayOff;
setMap = overlaySetMap;
@@ -19,6 +22,7 @@ class TestOverlay {
}
class TestMap {
constructor() { mapInstances.push(this); }
add = vi.fn();
addControl = vi.fn();
setFitView = vi.fn();
@@ -90,6 +94,38 @@ afterEach(() => {
mapOff.mockReset();
mapDestroy.mockReset();
markerListeners.length = 0;
overlayInstances.length = 0;
mapInstances.length = 0;
});
test('uses the playback cadence for follow animation and advances the passed path', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
const view = render(<TrackMap
points={points}
stops={[]}
activeIndex={0}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances).toHaveLength(1));
view.rerender(<TrackMap
points={points}
stops={[]}
activeIndex={1}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances[0].panTo).toHaveBeenLastCalledWith(expect.any(Array), 65));
expect(overlayInstances.some((overlay) => overlay.setPath.mock.calls.some(([path]) => Array.isArray(path) && path.length === 2))).toBe(true);
});
test('detaches every marker listener and map listener on unmount', async () => {

View File

@@ -8,12 +8,13 @@ function markerContent(kind: 'start' | 'end' | 'stop' | 'current', label?: strin
return `<div class="v2-track-marker is-${kind}"><span>${label ?? ''}</span></div>`;
}
export function TrackMap({ points, stops, activeIndex, showStops, follow, onSelectIndex, onFollowChange }: {
export function TrackMap({ points, stops, activeIndex, showStops, follow, followDurationMs = 180, onSelectIndex, onFollowChange }: {
points: HistoryLocationRow[];
stops: TrackStop[];
activeIndex: number;
showStops: boolean;
follow: boolean;
followDurationMs?: number;
onSelectIndex: (index: number) => void;
onFollowChange: (follow: boolean) => void;
}) {
@@ -29,6 +30,15 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
const followChangeRef = useRef(onFollowChange);
const [state, setState] = useState<'loading' | 'ready' | 'fallback' | 'error'>('loading');
const valid = useMemo(() => points.map((point, index) => ({ point, index })).filter(({ point }) => isValidAMapCoordinate(point.longitude, point.latitude)), [points]);
const passedCountByPoint = useMemo(() => {
const counts = new Uint16Array(points.length);
let validIndex = 0;
for (let pointIndex = 0; pointIndex < points.length; pointIndex += 1) {
while (validIndex < valid.length && valid[validIndex].index <= pointIndex) validIndex += 1;
counts[pointIndex] = validIndex;
}
return counts;
}, [points.length, valid]);
useEffect(() => { selectRef.current = onSelectIndex; }, [onSelectIndex]);
useEffect(() => { followChangeRef.current = onFollowChange; }, [onFollowChange]);
@@ -127,10 +137,10 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
if (!point || !isValidAMapCoordinate(point.longitude, point.latitude)) return;
const position = wgs84ToGcj02(point.longitude, point.latitude);
currentMarkerRef.current?.setPosition?.(position);
const passedCount = valid.reduce((count, entry) => count + (entry.index <= activeIndex ? 1 : 0), 0);
const passedCount = passedCountByPoint[activeIndex] ?? 0;
passedPathRef.current?.setPath?.(pathRef.current.slice(0, Math.max(1, passedCount)));
if (follow) mapRef.current?.panTo?.(position, 180);
}, [activeIndex, follow, points, valid]);
if (follow) mapRef.current?.panTo?.(position, followDurationMs);
}, [activeIndex, follow, followDurationMs, passedCountByPoint, points]);
return <div className="v2-track-map">
<div ref={containerRef} className="v2-track-map-canvas" aria-label="历史轨迹地图" />

View File

@@ -8,7 +8,7 @@ import { ROUTER_FUTURE } from '../routing/routerConfig';
const mocks = vi.hoisted(() => ({ trackPlayback: vi.fn(), reverseGeocode: vi.fn(), vehicles: vi.fn() }));
vi.mock('../../api/client', () => ({ api: mocks }));
vi.mock('../map/TrackMap', () => ({ TrackMap: ({ activeIndex }: { activeIndex: number }) => <div data-testid="track-map" data-active-index={activeIndex} /> }));
vi.mock('../map/TrackMap', () => ({ TrackMap: ({ activeIndex, followDurationMs }: { activeIndex: number; followDurationMs: number }) => <div data-testid="track-map" data-active-index={activeIndex} data-follow-duration={followDurationMs} /> }));
const track = {
vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z',
@@ -55,6 +55,12 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2));
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } });
fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' }));
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '65');
fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' }));
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180');
fireEvent.click(screen.getByRole('button', { name: '收起查询面板' }));
expect(screen.getByRole('button', { name: /查询与明细/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /查询与明细/ }));

View File

@@ -3,11 +3,11 @@ import {
IconBox, IconChevronLeft, IconChevronRight, IconClose, IconDownload, IconEyeClosed,
IconEyeOpened, IconList, IconMapPin, IconPause, IconPlay, IconRefresh, IconSearch
} from '@douyinfe/semi-icons';
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import { FormEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../../api/types';
import { downloadTrackCsv, formatDuration, sampledEventIndex } from '../domain/track';
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, trackPlaybackInterval } from '../domain/track';
import { TrackMap } from '../map/TrackMap';
import { InlineError } from '../shared/AsyncState';
import { QUERY_MEMORY } from '../queryPolicy';
@@ -16,9 +16,18 @@ const speedOptions = [0.5, 1, 2, 4] as const;
type PlaybackSpeed = (typeof speedOptions)[number];
type PanelTab = 'stops' | 'events' | 'overview';
type Draft = { keyword: string; dateFrom: string; dateTo: string; protocol: string };
const EMPTY_INDEXES: readonly number[] = [];
const numberFormatters = new Map<number, Intl.NumberFormat>();
const timeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateTimeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
function number(value: number, digits = 1) {
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits }).format(Number.isFinite(value) ? value : 0);
let formatter = numberFormatters.get(digits);
if (!formatter) {
formatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits });
numberFormatters.set(digits, formatter);
}
return formatter.format(Number.isFinite(value) ? value : 0);
}
function direction(value?: number) {
@@ -36,14 +45,14 @@ function alarm(value?: number) {
function time(value?: string) {
if (!value) return '—';
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(parsed);
if (!Number.isNaN(parsed.getTime())) return timeFormatter.format(parsed);
return value.split(' ').pop()?.slice(0, 8) || '—';
}
function dateTime(value?: string) {
if (!value) return '—';
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(parsed);
if (!Number.isNaN(parsed.getTime())) return dateTimeFormatter.format(parsed);
return value.replace('T', ' ').slice(5, 19);
}
@@ -128,10 +137,11 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
</div>;
}
function TrackRail({ draft, track, activeIndex, tab, onDraft, onSubmit, onTab, onSelectIndex, onCollapse }: {
const TrackRail = memo(function TrackRail({ draft, track, activeStopIndexes, activeEventIndexes, tab, onDraft, onSubmit, onTab, onSelectIndex, onCollapse }: {
draft: Draft;
track?: TrackPlaybackResponse;
activeIndex: number;
activeStopIndexes: readonly number[];
activeEventIndexes: readonly number[];
tab: PanelTab;
onDraft: (draft: Draft) => void;
onSubmit: (event: FormEvent) => void;
@@ -155,15 +165,15 @@ function TrackRail({ draft, track, activeIndex, tab, onDraft, onSubmit, onTab, o
<nav aria-label="轨迹明细分类"><button type="button" className={tab === 'stops' ? 'is-active' : ''} onClick={() => onTab('stops')}> <b>{track?.stops.length ?? 0}</b></button><button type="button" className={tab === 'events' ? 'is-active' : ''} onClick={() => onTab('events')}> <b>{track?.events.length ?? 0}</b></button><button type="button" className={tab === 'overview' ? 'is-active' : ''} onClick={() => onTab('overview')}></button></nav>
<div className="v2-track-rail-scroll">
{!track ? <div className="v2-track-rail-empty"><IconMapPin /><strong></strong><p></p></div> : null}
{track && tab === 'stops' ? <div className="v2-track-stop-list">{track.stops.map((stop, index) => <button type="button" className={Math.abs(stop.sampledIndex - activeIndex) < 2 ? 'is-active' : ''} key={`${stop.startTime}-${index}`} onClick={() => onSelectIndex(stop.sampledIndex)}><i>{index + 1}</i><span><strong>{dateTime(stop.startTime)}</strong><small> {formatDuration(stop.durationSeconds)} · {stop.pointCount} </small></span><em>{time(stop.endTime)}</em></button>)}{!track.stops.length ? <p className="v2-track-list-empty"> 3 </p> : null}</div> : null}
{track && tab === 'events' ? <div className="v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <button type="button" className={sampled === activeIndex ? 'is-active' : ''} key={`${event.type}-${event.time}-${index}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></button>; })}</div> : null}
{track && tab === 'stops' ? <div className="v2-track-stop-list">{track.stops.map((stop, index) => <button type="button" className={activeStopIndexes.includes(index) ? 'is-active' : ''} key={`${stop.startTime}-${index}`} onClick={() => onSelectIndex(stop.sampledIndex)}><i>{index + 1}</i><span><strong>{dateTime(stop.startTime)}</strong><small> {formatDuration(stop.durationSeconds)} · {stop.pointCount} </small></span><em>{time(stop.endTime)}</em></button>)}{!track.stops.length ? <p className="v2-track-list-empty"> 3 </p> : null}</div> : null}
{track && tab === 'events' ? <div className="v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <button type="button" className={activeEventIndexes.includes(index) ? 'is-active' : ''} key={`${event.type}-${event.time}-${index}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></button>; })}</div> : null}
{track && tab === 'overview' ? <OverviewPanel track={track} /> : null}
</div>
</div>
</aside>;
}
});
function SegmentRail({ track, onSelectIndex }: { track: TrackPlaybackResponse; onSelectIndex: (index: number) => void }) {
const SegmentRail = memo(function SegmentRail({ track, onSelectIndex }: { track: TrackPlaybackResponse; onSelectIndex: (index: number) => void }) {
const segments = track.segments.slice(0, 160);
const total = Math.max(1, segments.reduce((sum, segment) => sum + Math.max(1, segment.durationSeconds), 0));
return <div className="v2-track-segment-rail" aria-label="轨迹活动分段">{segments.map((segment) => <button
@@ -173,7 +183,7 @@ function SegmentRail({ track, onSelectIndex }: { track: TrackPlaybackResponse; o
title={`${segment.title} · ${formatDuration(segment.durationSeconds)} · ${number(segment.distanceKm)} km`}
type="button"
/>)}</div>;
}
});
export default function TrackPage() {
const [searchParams, setSearchParams] = useSearchParams();
@@ -208,6 +218,9 @@ export default function TrackPage() {
const points = track?.points ?? [];
const boundedIndex = Math.min(activeIndex, Math.max(points.length - 1, 0));
const current = points[boundedIndex];
const railSelection = useMemo(() => buildTrackRailSelection(track), [track]);
const activeStopIndexes = railSelection.stopIndexesByPoint[boundedIndex] ?? EMPTY_INDEXES;
const activeEventIndexes = railSelection.eventIndexesByPoint[boundedIndex] ?? EMPTY_INDEXES;
const [addressPoint, setAddressPoint] = useState<{ longitude: number; latitude: number }>();
useEffect(() => {
@@ -222,11 +235,11 @@ export default function TrackPage() {
gcTime: QUERY_MEMORY.highVolumeGcTime
});
useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]);
useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); setAddressPoint(undefined); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]);
useEffect(() => {
if (!playing || points.length < 2) return;
lastFrameRef.current = 0;
const interval = Math.max(55, 300 / playbackSpeed);
const interval = trackPlaybackInterval(playbackSpeed);
const tick = (timestamp: number) => {
if (!lastFrameRef.current) lastFrameRef.current = timestamp;
if (timestamp - lastFrameRef.current >= interval) {
@@ -244,7 +257,7 @@ export default function TrackPage() {
return () => { if (animationRef.current) window.cancelAnimationFrame(animationRef.current); };
}, [playing, playbackSpeed, points.length]);
const submit = (event: FormEvent) => {
const submit = useCallback((event: FormEvent) => {
event.preventDefault();
const keyword = draft.keyword.trim();
if (!keyword) return;
@@ -255,8 +268,9 @@ export default function TrackPage() {
if (next.dateTo) url.set('dateTo', next.dateTo);
if (next.protocol) url.set('protocol', next.protocol);
setSearchParams(url, { replace: true });
};
const selectIndex = (index: number) => { setPlaying(false); setActiveIndex(Math.max(0, Math.min(points.length - 1, index))); };
}, [draft, setSearchParams]);
const selectIndex = useCallback((index: number) => { setPlaying(false); setActiveIndex(Math.max(0, Math.min(points.length - 1, index))); }, [points.length]);
const collapseRail = useCallback(() => setRailCollapsed(true), []);
const togglePlayback = () => {
if (points.length < 2) return;
if (!playing && boundedIndex >= points.length - 1) setActiveIndex(0);
@@ -266,9 +280,9 @@ export default function TrackPage() {
const progress = points.length > 1 ? boundedIndex / (points.length - 1) * 100 : 0;
return <div className={`v2-track-page${railCollapsed ? ' is-rail-collapsed' : ''}`}>
<TrackRail draft={draft} track={track} activeIndex={boundedIndex} tab={panelTab} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectIndex} onCollapse={() => setRailCollapsed(true)} />
<TrackRail draft={draft} track={track} activeStopIndexes={activeStopIndexes} activeEventIndexes={activeEventIndexes} tab={panelTab} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectIndex} onCollapse={collapseRail} />
<section className="v2-track-stage">
<TrackMap points={points} stops={track?.stops ?? []} activeIndex={boundedIndex} showStops={showStops} follow={follow} onSelectIndex={selectIndex} onFollowChange={setFollow} />
<TrackMap points={points} stops={track?.stops ?? []} activeIndex={boundedIndex} showStops={showStops} follow={follow} followDurationMs={playing ? Math.max(40, trackPlaybackInterval(playbackSpeed) - 10) : 180} onSelectIndex={selectIndex} onFollowChange={setFollow} />
<div className="v2-track-stage-tools">
{railCollapsed ? <button type="button" aria-label="展开查询与明细" onClick={() => setRailCollapsed(false)}><IconList /><span></span></button> : null}

View File

@@ -8,6 +8,14 @@ The 15-second monitor refresh previously treated every new response object as a
FleetMap now fingerprints only fields that affect map rendering. An unchanged refresh performs no AMap data/style/label mutation. A moving vehicle still updates MassMarks once, but the LabelsLayer removes and replaces only that vehicle's changed marker; full label-layer replacement is reserved for crossing the normal/dense zoom boundary. The selected ripple marker also skips redundant position writes. This uses the official AMap JS API 2.0 [`LabelsLayer.remove`](https://lbs.amap.com/api/javascript-api-v2/guide/amap-massmarker/label-marker) and `LabelMarker` update model instead of reconstructing all overlays. Unit tests assert zero map mutations for an `asOf`-only refresh and exactly one label replacement for one moved vehicle.
## 2026-07-16: bounded track-playback rendering
The production replay for a 1,600-point day contained 84 interactive activity segments, nine stop rows and 64 event rows. Advancing `activeIndex` previously rendered the complete query/result rail and all 84 segment buttons on every playback step. It also rebuilt `Intl.NumberFormat` and `Intl.DateTimeFormat` instances during render, counted passed map points with a full-array reduction, and at 4× speed started a 180 ms map pan every 75 ms. The overlapping camera animations and repeated static work competed with the marker update on the main thread.
Playback now precomputes sparse stop/event highlight indexes once per track. The memoized result rail receives a shared empty selection for ordinary points, while the memoized segment rail receives stable callbacks, so both static subtrees skip the vast majority of playback renders. Date and number formatters are module-level reusable instances, and the passed-path count is a precomputed O(1) lookup instead of an O(points) scan per step. Follow animation is cadence-bound: 4× playback uses 65 ms movement inside its 75 ms update interval, while direct operator selection retains the smoother 180 ms transition.
This follows React's requirement that memoized children receive stable props and callbacks, and the browser animation model that schedules work against paint frames and calculates progress from timestamps: <https://react.dev/reference/react/memo>, <https://react.dev/reference/react/useMemo>, <https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame>. The regression suite asserts sparse highlight mapping, the 55 ms minimum playback interval, 65 ms 4× follow duration, 180 ms paused duration, passed-path advancement and complete map/listener cleanup.
## 2026-07-16: single responsive mileage matrix
The mileage query rendered its desktop table and mobile card matrix at the same time, then hid one tree with CSS. A 90-day, 20-vehicle production page therefore retained 1,860 desktop cells and another 1,800 hidden mobile day cells. Chrome identifies excessive DOM size as a source of longer style calculation, layout and interaction work, and recommends creating nodes only when they are needed: <https://developer.chrome.com/docs/performance/insights/dom-size>, <https://web.dev/articles/dom-size-and-interactivity>.