9.6 KiB
Frontend production-readiness audit
This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready.
2026-07-16: 50-cycle authenticated route soak
The production ECS application completed 50 authenticated monitor -> history -> statistics -> tracks -> monitor cycles. Chrome performance and heap metrics were sampled every five cycles. Because the Browser channel does not permit HeapProfiler.collectGarbage, the gate uses post-warmup and 15-second-idle low-water marks instead of comparing transient allocation peaks.
| Metric | After 10 warm cycles and natural collection | After 50 cycles and 15 seconds idle | Result |
|---|---|---|---|
| JS heap used | 26.52 MB | 28.53 MB | Stable; +2.01 MB |
| Event listeners | 880 | 686 | No monotonic growth |
| DOM nodes | 7,763 | 4,811 | No monotonic growth |
| Detached script states | 0 | 0 | Fully released |
| Live page DOM | — | 1 AMap iframe / 1 map container | Only the active monitor map remains |
Transient peaks reached 165.83 MB while route effects and AMap instances overlapped, then returned to the low-water mark after natural collection. The final page remained /monitor, rendered live fleet statistics, and had zero console warnings or errors. This proves that the explicit query, map and listener cleanup prevents sustained growth under this route pattern; it does not replace a future allocation profile for data-heavy operator interactions.
The audit found one remaining lifecycle asymmetry: track start, end and stop markers registered anonymous click handlers and relied on overlay removal to release them. Track overlays now retain each handler identity and call off('click', handler) before redraw and unmount. TrackMap.test.tsx asserts every marker handler, the map drag handler, overlays and the map instance are released.
2026-07-16: monitor lifecycle and responsive rendering
The monitor's high-volume fleet, map and selected-vehicle queries now use immediate inactive-cache collection instead of inheriting the global five-minute retention. The detail card owns its detail, alert and address queries, so collapsing or clearing the card unmounts its observers while the separate selected-vehicle stream continues to move the map. Repeated viewport and vehicle-selection tests assert that only the active payload remains and that detail-card queries reach zero after unmount.
The realtime list no longer renders the desktop table and mobile cards simultaneously. A viewport listener mounts exactly one representation, cutting a 50-row page from 100 row trees and 100 address query observers to 50. The listener is removed with the component. Fleet and track maps also keep stable event-handler identities and pair every AMap on with off before overlay removal and destroy().
This follows TanStack Query's documented behavior that inactive queries remain cached for five minutes unless gcTime changes, and AMap JS API 2.0's requirement to unbind the same handler object with off that was registered with on:
- https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults
- https://lbs.amap.com/api/javascript-api-v2/guide/events/map_overlay
2026-07-16: production stylesheet boundary
The production entry renders AppV2 exclusively. V2 pages do not import Semi UI components, so loading the legacy global.css from main.tsx made every route download and parse the complete legacy Semi theme and thousands of unreachable V1 rules. The entry now imports only v2/styles/v2.css; the legacy stylesheet remains available to the explicit test:legacy compatibility suite but cannot enter the V2 production bundle. productionEntry.test.ts protects this boundary. The verified production CSS fell from 435.20 kB / 53.50 kB gzip to 168.91 kB / 27.72 kB gzip, and the Semi theme Sass deprecation warnings disappeared from the build.
The default test command is now the production V2 suite. The old App.tsx integration test remains runnable as test:legacy, and test:all is retained for combined audits. This separation makes deprecation and jsdom navigation warnings from the retired Semi UI shell visible without allowing them to hide regressions in the production application. The 2026-07-16 baseline is 139/139 passing production tests and 154/168 passing legacy assertions; the 14 legacy failures describe retired V1 mileage and hash-route expectations and do not gate V2 deployment.
2026-07-15: route continuity and monitor memory
Resolved in this round
| Risk | User-visible failure | Control | Evidence gate |
|---|---|---|---|
| A stale lazy route asset disappears during release | Navigation leaves the content area blank | Preserve the previous build's original hashed assets for one generation; isolate every route with a recoverable error boundary and one-shot reload | Request an old route asset after the symlink switch; navigate through every primary route without a blank shell |
| A pasted batch search temporarily shows the previous fleet | Operators may mistake stale vehicles for batch matches | Hide placeholder rows while keywords is changing; report matched and missing plates after the request resolves |
Paste newline/comma-separated plates with a duplicate and an unknown plate; verify exact results and missing-count feedback |
| URL filters remount the whole route | Pagination or tab changes lose local state and may flash a loading screen | Key the route recovery boundary by pathname only; keep the full URL only for chunk-reload diagnostics | Change search parameters inside a stateful route and assert one mount with preserved local state |
| Route-scoped reads continue after navigation | Obsolete responses consume bandwidth and remain in memory | Consume TanStack Query's AbortSignal in every V2 read path; immediately collect high-volume access and alert pages after they become inactive |
Start a read, navigate away or change its query key, and assert the fetch receives the query signal |
| A route is first fetched only after click | Slow transitions and a longer loading flash | Deduplicated route import cache plus pointer, focus and pointer-down preload | Route module unit tests and rendered navigation loop |
| Rapid map pans retain large query payloads | Heap growth and eventual interaction jank | Consume React Query AbortSignal and immediately garbage-collect inactive map viewport queries |
Query-cache churn test keeps exactly one map payload after repeated viewport changes |
| Superseded monitor requests continue in the background | Wasted server traffic and stale responses | Pass request cancellation signals through the API client | API client signal test and browser network/console smoke |
| QR generation resolves after the dialog unmounts | State update after lifecycle end | Cancel the asynchronous state write on cleanup | Monitor component test |
React documents that lazy caches the import promise and propagates a rejected module load to the nearest Error Boundary; this is why a route-level boundary is required rather than a loading fallback alone: https://react.dev/reference/react/lazy. React's Suspense documentation also distinguishes a loading fallback from error handling: https://react.dev/reference/react/Suspense.
The navigation and progressive-loading direction follows mature observability systems: persistent global controls, collapsible sections and loading only the content needed for the current task. Elastic documents these dashboard interaction and panel-organization patterns here: https://www.elastic.co/docs/explore-analyze/dashboards/using and https://www.elastic.co/docs/explore-analyze/dashboards/arrange-panels.
Remaining audit queue
- Capture an allocation profile for data-heavy operator interactions: loaded history series, long track playback and repeated Excel exports.
- Bound or explicitly evict other high-cardinality query families, especially arbitrary history windows and track playback ranges.
- Retire or migrate the remaining legacy
App.tsxintegration suite after its compatibility coverage is replaced in V2. - Preserve ExcelJS as an action-only dynamic import and consider moving large workbook generation off the main thread.
- Add an automated release smoke that reads the previous
.release-assetsmanifest and asserts every listed compatibility asset returns HTTP 200.
2026-07-15: query memory and vehicle-count semantics
Production reconciliation proved that the three visible vehicle counts represent different populations rather than a data loss: 1,035 service identities equal 1,024 bound master vehicles plus 11 realtime identities awaiting binding; 738 vehicles currently have a realtime location row and can enter the global-monitor map. The UI must name these populations explicitly instead of presenting them as interchangeable totals.
High-volume history rows, aggregated series, track playback and daily-mileage matrices consume the request AbortSignal and use zero inactive-cache retention. Small vehicle-option and summary results retain only a bounded 30–60 second cache. This preserves responsive repeated lookups without retaining multiple large arbitrary date-window payloads after their observer is gone.
Release evidence template
- Commit and release identifier
- All non-legacy frontend tests
- Production V2 route tests
- TypeScript/Vite production build sizes
- Authenticated health response and
data.runtime.platformRelease - New main asset HTTP status
- Previous route asset HTTP status
- Browser route loop, blank-page check and console warnings/errors
- Desktop and mobile viewport screenshots