perf(web): contain offscreen vehicle rows

This commit is contained in:
lingniu
2026-07-16 02:51:12 +08:00
parent 7396544269
commit 02868344ca
3 changed files with 28 additions and 3 deletions

View File

@@ -1,9 +1,32 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import mainSource from '../main.tsx?raw'; import mainSource from '../main.tsx?raw';
const v2Styles = readFileSync(resolve(process.cwd(), 'src/v2/styles/v2.css'), 'utf8');
describe('V2 production entry', () => { describe('V2 production entry', () => {
test('does not load the legacy Semi UI theme and global stylesheet', () => { test('does not load the legacy Semi UI theme and global stylesheet', () => {
expect(mainSource).not.toContain("./styles/global.css"); expect(mainSource).not.toContain("./styles/global.css");
expect(mainSource).toContain("./v2/styles/v2.css"); expect(mainSource).toContain("./v2/styles/v2.css");
}); });
test('applies rendering containment to off-screen vehicle items instead of the visible scroller', () => {
const ruleBody = (selector: string) => {
const start = v2Styles.indexOf(`${selector} {`);
expect(start).toBeGreaterThanOrEqual(0);
const end = v2Styles.indexOf('}', start);
expect(end).toBeGreaterThan(start);
return v2Styles.slice(start, end);
};
const scrollRule = ruleBody('.v2-vehicle-scroll');
const rowRule = ruleBody('.v2-vehicle-row');
const mobileCardRule = ruleBody('.v2-monitor-mobile-cards > article');
expect(scrollRule).not.toContain('content-visibility');
expect(rowRule).toContain('content-visibility: auto');
expect(rowRule).toContain('contain-intrinsic-size: auto 60px');
expect(mobileCardRule).toContain('content-visibility: auto');
expect(mobileCardRule).toContain('contain-intrinsic-size: auto 154px');
});
}); });

View File

@@ -108,8 +108,8 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-vehicle-rail > header span, .v2-vehicle-rail > footer { color: var(--v2-muted); font-size: 10px; } .v2-vehicle-rail > header span, .v2-vehicle-rail > footer { color: var(--v2-muted); font-size: 10px; }
.v2-rail-search { display: flex; height: 34px; align-items: center; gap: 7px; margin: 0 9px 8px; border: 1px solid var(--v2-border); border-radius: 7px; padding: 0 9px; color: #8a98aa; font-size: 10px; } .v2-rail-search { display: flex; height: 34px; align-items: center; gap: 7px; margin: 0 9px 8px; border: 1px solid var(--v2-border); border-radius: 7px; padding: 0 9px; color: #8a98aa; font-size: 10px; }
.v2-rail-search.has-missing { border-color: #fed7aa; background: #fffaf3; color: #b45309; } .v2-rail-search.has-missing { border-color: #fed7aa; background: #fffaf3; color: #b45309; }
.v2-vehicle-scroll { min-height: 0; flex: 1; overflow: auto; overscroll-behavior: contain; content-visibility: auto; } .v2-vehicle-scroll { min-height: 0; flex: 1; overflow: auto; overscroll-behavior: contain; }
.v2-vehicle-row { display: grid; width: 100%; min-height: 60px; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; gap: 8px; border: 0; border-top: 1px solid #eef2f7; background: #fff; padding: 8px 10px; text-align: left; cursor: pointer; } .v2-vehicle-row { display: grid; width: 100%; min-height: 60px; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; gap: 8px; border: 0; border-top: 1px solid #eef2f7; background: #fff; padding: 8px 10px; text-align: left; cursor: pointer; content-visibility: auto; contain-intrinsic-size: auto 60px; }
.v2-vehicle-row:hover { background: #f8fbff; } .v2-vehicle-row:hover { background: #f8fbff; }
.v2-vehicle-row.is-selected { position: relative; z-index: 1; background: var(--v2-blue-soft); box-shadow: inset 3px 0 var(--v2-blue); } .v2-vehicle-row.is-selected { position: relative; z-index: 1; background: var(--v2-blue-soft); box-shadow: inset 3px 0 var(--v2-blue); }
.v2-status-dot { width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; } .v2-status-dot { width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; }
@@ -1119,7 +1119,7 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-monitor-table-panel > header { align-items: flex-start; flex-direction: column; gap: 4px; padding: 10px 12px; } .v2-monitor-table-panel > header { align-items: flex-start; flex-direction: column; gap: 4px; padding: 10px 12px; }
.v2-monitor-table-scroll { display: none; } .v2-monitor-table-scroll { display: none; }
.v2-monitor-mobile-cards { display: flex; min-height: 0; flex: 1; flex-direction: column; gap: 8px; overflow: auto; padding: 8px; background: #f5f7fa; } .v2-monitor-mobile-cards { display: flex; min-height: 0; flex: 1; flex-direction: column; gap: 8px; overflow: auto; padding: 8px; background: #f5f7fa; }
.v2-monitor-mobile-cards > article { border: 1px solid #e0e6ee; border-radius: 10px; background: #fff; padding: 12px; box-shadow: 0 2px 8px rgba(21,32,51,.04); } .v2-monitor-mobile-cards > article { border: 1px solid #e0e6ee; border-radius: 10px; background: #fff; padding: 12px; box-shadow: 0 2px 8px rgba(21,32,51,.04); content-visibility: auto; contain-intrinsic-size: auto 154px; }
.v2-monitor-mobile-cards > article > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; } .v2-monitor-mobile-cards > article > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.v2-monitor-mobile-cards > article > header div { display: flex; min-width: 0; flex-direction: column; gap: 3px; } .v2-monitor-mobile-cards > article > header div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.v2-monitor-mobile-cards > article > header strong { font-size: 15px; }.v2-monitor-mobile-cards > article > header span { color: #7f8da1; font-size: 10px; } .v2-monitor-mobile-cards > article > header strong { font-size: 15px; }.v2-monitor-mobile-cards > article > header span { color: #7f8da1; font-size: 10px; }

View File

@@ -88,6 +88,8 @@ React documents that `lazy` caches the import promise and propagates a rejected
The first continuity pass only warmed Monitor, Tracks, History and Statistics in the background. A production cold-cache audit with 450 ms simulated latency still measured roughly 1.66 seconds for `/monitor` to `/access`, leaving Vehicles, Alerts, Access and Operations dependent on click-time loading. The idle queue now covers all eight route sections in bounded batches of two. It still skips `saveData`, `slow-2g` and `2g` connections, while pointer/focus/down intent preloading remains the fastest path for an immediately selected destination. Unit coverage proves every inactive route is queued and that cleanup cancels the next idle batch. The first continuity pass only warmed Monitor, Tracks, History and Statistics in the background. A production cold-cache audit with 450 ms simulated latency still measured roughly 1.66 seconds for `/monitor` to `/access`, leaving Vehicles, Alerts, Access and Operations dependent on click-time loading. The idle queue now covers all eight route sections in bounded batches of two. It still skips `saveData`, `slow-2g` and `2g` connections, while pointer/focus/down intent preloading remains the fastest path for an immediately selected destination. Unit coverage proves every inactive route is queued and that cleanup cancels the next idle batch.
A production route-memory loop did not prove a retained application leak: after natural collection, JS heap returned to roughly its initial 31 MB range and old map documents/frames were released. It did expose an ineffective rendering hint: `content-visibility: auto` was attached to the always-visible vehicle scroll viewport instead of the 200 independently off-screen rows. The containment boundary now lives on every desktop rail row and mobile list card, paired with `contain-intrinsic-size` so skipped content keeps stable scroll geometry. This follows the CSS Containment specification's long-scroll-list use case while avoiding the documented scrollbar jump caused by missing intrinsic size: <https://www.w3.org/TR/css-contain-2/#content-visibility>.
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>. 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 ### Remaining audit queue