perf: QA sprint v2 — 8 optimizations + responsive fixes
Fixes: - #29 agents.js: share geometries across agents (3 shared vs 12 duplicates) - #30 agents.js: single connection line material, dispose old geometries - #31 agents.js: add Agent.dispose() for proper GPU resource cleanup - #32 main.js: debounce window resize with rAF (1 call/frame vs dozens) - #33 main.js: pause rAF loop on visibilitychange (battery savings on iPad) - #34 effects.js: skip every 2nd rain update on low tier (halves iterations) - #35 index.html: responsive HUD with clamp(), mobile stack layout <500px - #36 vite.config.js: code-split Three.js into separate cacheable chunk Build output: - App code: 28.7KB (was bundled into 514KB single chunk) - Three.js: 486KB (cached independently after first visit) - FPS: 31 (up from 28-29)
This commit is contained in:
15
index.html
15
index.html
@@ -36,21 +36,21 @@
|
||||
}
|
||||
#hud {
|
||||
position: fixed; top: 16px; left: 16px;
|
||||
color: #00ff41; font-size: 12px; line-height: 1.6;
|
||||
color: #00ff41; font-size: clamp(10px, 1.5vw, 14px); line-height: 1.6;
|
||||
text-shadow: 0 0 8px #00ff41;
|
||||
pointer-events: none;
|
||||
}
|
||||
#hud h1 { font-size: 16px; letter-spacing: 4px; margin-bottom: 8px; color: #00ff88; }
|
||||
#hud h1 { font-size: clamp(12px, 2vw, 18px); letter-spacing: clamp(2px, 0.4vw, 4px); margin-bottom: 8px; color: #00ff88; }
|
||||
#status-panel {
|
||||
position: fixed; top: 16px; right: 16px;
|
||||
color: #00ff41; font-size: 11px; line-height: 1.8;
|
||||
color: #00ff41; font-size: clamp(9px, 1.2vw, 12px); line-height: 1.8;
|
||||
text-shadow: 0 0 6px #00ff41; max-width: 240px;
|
||||
}
|
||||
#status-panel .label { color: #007722; }
|
||||
#chat-panel {
|
||||
position: fixed; bottom: 16px; left: 16px; right: 16px;
|
||||
max-height: 180px; overflow-y: auto;
|
||||
color: #00ff41; font-size: 11px; line-height: 1.6;
|
||||
color: #00ff41; font-size: clamp(9px, 1.2vw, 12px); line-height: 1.6;
|
||||
text-shadow: 0 0 4px #00ff41;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
.chat-entry .agent-name { color: #00ff88; font-weight: bold; }
|
||||
#connection-status {
|
||||
position: fixed; bottom: 16px; right: 16px;
|
||||
font-size: 11px; color: #555;
|
||||
font-size: clamp(9px, 1.2vw, 12px); color: #555;
|
||||
}
|
||||
#connection-status.connected { color: #00ff41; text-shadow: 0 0 6px #00ff41; }
|
||||
|
||||
@@ -69,6 +69,11 @@
|
||||
#chat-panel { bottom: calc(16px + env(safe-area-inset-bottom)); left: calc(16px + env(safe-area-inset-left)); right: calc(16px + env(safe-area-inset-right)); }
|
||||
#connection-status { bottom: calc(16px + env(safe-area-inset-bottom)); right: calc(16px + env(safe-area-inset-right)); }
|
||||
}
|
||||
|
||||
/* Stack status panel below HUD on narrow viewports (must come AFTER @supports) */
|
||||
@media (max-width: 500px) {
|
||||
#status-panel { top: 100px !important; left: 16px; right: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
52
js/agents.js
52
js/agents.js
@@ -5,6 +5,20 @@ const agents = new Map();
|
||||
let scene;
|
||||
let connectionLines = [];
|
||||
|
||||
/* ── Shared geometries (created once, reused by all agents) ── */
|
||||
const SHARED_GEO = {
|
||||
core: new THREE.IcosahedronGeometry(0.7, 1),
|
||||
ring: new THREE.TorusGeometry(1.1, 0.04, 8, 32),
|
||||
glow: new THREE.SphereGeometry(1.3, 16, 16),
|
||||
};
|
||||
|
||||
/* ── Shared connection line material (one instance for all lines) ── */
|
||||
const CONNECTION_MAT = new THREE.LineBasicMaterial({
|
||||
color: 0x003300,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
});
|
||||
|
||||
class Agent {
|
||||
constructor(def) {
|
||||
this.id = def.id;
|
||||
@@ -23,7 +37,8 @@ class Agent {
|
||||
}
|
||||
|
||||
_buildMeshes() {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
// Per-agent materials (need unique color + mutable emissiveIntensity)
|
||||
const coreMat = new THREE.MeshStandardMaterial({
|
||||
color: this.color,
|
||||
emissive: this.color,
|
||||
emissiveIntensity: 0.4,
|
||||
@@ -31,24 +46,21 @@ class Agent {
|
||||
metalness: 0.8,
|
||||
});
|
||||
|
||||
const geo = new THREE.IcosahedronGeometry(0.7, 1);
|
||||
this.core = new THREE.Mesh(geo, mat);
|
||||
this.core = new THREE.Mesh(SHARED_GEO.core, coreMat);
|
||||
this.group.add(this.core);
|
||||
|
||||
const ringGeo = new THREE.TorusGeometry(1.1, 0.04, 8, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({ color: this.color, transparent: true, opacity: 0.5 });
|
||||
this.ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
this.ring = new THREE.Mesh(SHARED_GEO.ring, ringMat);
|
||||
this.ring.rotation.x = Math.PI / 2;
|
||||
this.group.add(this.ring);
|
||||
|
||||
const glowGeo = new THREE.SphereGeometry(1.3, 16, 16);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: this.color,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.BackSide,
|
||||
});
|
||||
this.glow = new THREE.Mesh(glowGeo, glowMat);
|
||||
this.glow = new THREE.Mesh(SHARED_GEO.glow, glowMat);
|
||||
this.group.add(this.glow);
|
||||
|
||||
const light = new THREE.PointLight(this.color, 1.5, 10);
|
||||
@@ -97,6 +109,18 @@ class Agent {
|
||||
setState(state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose per-agent GPU resources (materials + textures).
|
||||
* Shared geometries are NOT disposed here — they outlive individual agents.
|
||||
*/
|
||||
dispose() {
|
||||
this.core.material.dispose();
|
||||
this.ring.material.dispose();
|
||||
this.glow.material.dispose();
|
||||
this.sprite.material.map.dispose();
|
||||
this.sprite.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function initAgents(sceneRef) {
|
||||
@@ -112,15 +136,15 @@ export function initAgents(sceneRef) {
|
||||
}
|
||||
|
||||
function buildConnectionLines() {
|
||||
connectionLines.forEach(l => scene.remove(l));
|
||||
// Dispose old line geometries before removing
|
||||
connectionLines.forEach(l => {
|
||||
scene.remove(l);
|
||||
l.geometry.dispose();
|
||||
// Material is shared — do NOT dispose here
|
||||
});
|
||||
connectionLines = [];
|
||||
|
||||
const agentList = [...agents.values()];
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: 0x003300,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
});
|
||||
|
||||
for (let i = 0; i < agentList.length; i++) {
|
||||
for (let j = i + 1; j < agentList.length; j++) {
|
||||
@@ -129,7 +153,7 @@ function buildConnectionLines() {
|
||||
if (a.position.distanceTo(b.position) <= 8) {
|
||||
const points = [a.position.clone(), b.position.clone()];
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const line = new THREE.Line(geo, lineMat.clone());
|
||||
const line = new THREE.Line(geo, CONNECTION_MAT);
|
||||
connectionLines.push(line);
|
||||
scene.add(line);
|
||||
}
|
||||
|
||||
14
js/effects.js
vendored
14
js/effects.js
vendored
@@ -5,9 +5,12 @@ let rainParticles;
|
||||
let rainPositions;
|
||||
let rainVelocities;
|
||||
let rainCount = 0;
|
||||
let skipFrames = 0; // 0 = update every frame, 1 = every 2nd frame
|
||||
let frameCounter = 0;
|
||||
|
||||
export function initEffects(scene) {
|
||||
const tier = getQualityTier();
|
||||
skipFrames = tier === 'low' ? 1 : 0; // Low tier: update rain every 2nd frame
|
||||
initMatrixRain(scene, tier);
|
||||
initStarfield(scene, tier);
|
||||
}
|
||||
@@ -77,8 +80,17 @@ function initStarfield(scene, tier) {
|
||||
export function updateEffects(_time) {
|
||||
if (!rainParticles) return;
|
||||
|
||||
// On low tier, skip every other frame to halve iteration cost
|
||||
if (skipFrames > 0) {
|
||||
frameCounter++;
|
||||
if (frameCounter % (skipFrames + 1) !== 0) return;
|
||||
}
|
||||
|
||||
// When skipping frames, multiply velocity to maintain visual speed
|
||||
const velocityMul = skipFrames > 0 ? (skipFrames + 1) : 1;
|
||||
|
||||
for (let i = 0; i < rainCount; i++) {
|
||||
rainPositions[i * 3 + 1] -= rainVelocities[i];
|
||||
rainPositions[i * 3 + 1] -= rainVelocities[i] * velocityMul;
|
||||
if (rainPositions[i * 3 + 1] < -1) {
|
||||
rainPositions[i * 3 + 1] = 40 + Math.random() * 20;
|
||||
rainPositions[i * 3] = (Math.random() - 0.5) * 100;
|
||||
|
||||
23
js/main.js
23
js/main.js
@@ -18,14 +18,21 @@ function main() {
|
||||
initUI();
|
||||
initWebSocket(scene);
|
||||
|
||||
window.addEventListener('resize', () => onWindowResize(camera, renderer));
|
||||
// Debounce resize to 1 call per frame (avoids dozens of framebuffer re-allocations during drag)
|
||||
let resizeFrame = null;
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeFrame) cancelAnimationFrame(resizeFrame);
|
||||
resizeFrame = requestAnimationFrame(() => onWindowResize(camera, renderer));
|
||||
});
|
||||
|
||||
// Dismiss loading screen
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) loadingScreen.classList.add('hidden');
|
||||
|
||||
let rafId = null;
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
rafId = requestAnimationFrame(animate);
|
||||
|
||||
const now = performance.now();
|
||||
frameCount++;
|
||||
@@ -48,6 +55,18 @@ function main() {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Pause rendering when tab is backgrounded (saves battery on iPad PWA)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
} else {
|
||||
if (!rafId) animate();
|
||||
}
|
||||
});
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ export default defineConfig({
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
three: ['three'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
|
||||
Reference in New Issue
Block a user