= ({ players, getColor, onSelectPlayer, sh
txRef.current.offY = d.startOffY + (e.clientY - d.sy);
applyTransform();
}
- // Coordinate display (throttled by React's batching)
- if (containerRef.current && imgSize.w > 0) {
+ // Coordinate display — direct DOM write, no React state
+ if (containerRef.current && imgSize.w > 0 && coordRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const tx = txRef.current;
const coord = pxToWorld(e.clientX - rect.left, e.clientY - rect.top, tx.scale, tx.offX, tx.offY, imgSize.w, imgSize.h);
- setWorldCoord(coord);
+ coordRef.current.textContent = formatCoord(coord.ns, coord.ew);
}
};
const onMouseUp = () => { dragRef.current.dragging = false; };
@@ -155,9 +155,7 @@ export const MapView: React.FC = ({ players, getColor, onSelectPlayer, sh
)}
- {worldCoord && (
- {formatCoord(worldCoord.ns, worldCoord.ew)}
- )}
+
);
};
diff --git a/frontend/src/components/map/Sidebar.tsx b/frontend/src/components/map/Sidebar.tsx
index 798eb5a2..0b22dd06 100644
--- a/frontend/src/components/map/Sidebar.tsx
+++ b/frontend/src/components/map/Sidebar.tsx
@@ -33,8 +33,9 @@ export const Sidebar: React.FC = ({
const isOnline = serverHealth?.status?.toLowerCase() === 'online' || serverHealth?.status?.toLowerCase() === 'up';
- // Defer player list rendering — kill counters don't need 30fps updates
+ // Defer player list rendering — sidebar stats don't need real-time updates
const deferredPlayers = useDeferredValue(players);
+ const deferredVitals = useDeferredValue(vitals);
const sorted = useMemo(() => {
let list = [...deferredPlayers];
@@ -110,7 +111,7 @@ export const Sidebar: React.FC = ({
diff --git a/frontend/src/components/windows/WindowRenderer.tsx b/frontend/src/components/windows/WindowRenderer.tsx
index 35b5d061..af9e7e67 100644
--- a/frontend/src/components/windows/WindowRenderer.tsx
+++ b/frontend/src/components/windows/WindowRenderer.tsx
@@ -1,14 +1,14 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, lazy, Suspense } from 'react';
import { useWindowManager } from '../../contexts/WindowManagerContext';
-import { ChatWindow } from './ChatWindow';
-import { StatsWindow } from './StatsWindow';
-import { CharacterWindow } from './CharacterWindow';
-import { InventoryWindow } from './InventoryWindow';
-import { RadarWindow } from './RadarWindow';
-import { CombatStatsWindow } from './CombatStatsWindow';
-import { CombatPickerWindow } from './CombatPickerWindow';
-import { IssuesWindow } from './IssuesWindow';
-import { VitalSharingWindow } from './VitalSharingWindow';
+import { ChatWindow } from './ChatWindow'; // Chat is always fast — keep eager
+const StatsWindow = lazy(() => import('./StatsWindow').then(m => ({ default: m.StatsWindow })));
+const CharacterWindow = lazy(() => import('./CharacterWindow').then(m => ({ default: m.CharacterWindow })));
+const InventoryWindow = lazy(() => import('./InventoryWindow').then(m => ({ default: m.InventoryWindow })));
+const RadarWindow = lazy(() => import('./RadarWindow').then(m => ({ default: m.RadarWindow })));
+const CombatStatsWindow = lazy(() => import('./CombatStatsWindow').then(m => ({ default: m.CombatStatsWindow })));
+const CombatPickerWindow = lazy(() => import('./CombatPickerWindow').then(m => ({ default: m.CombatPickerWindow })));
+const IssuesWindow = lazy(() => import('./IssuesWindow').then(m => ({ default: m.IssuesWindow })));
+const VitalSharingWindow = lazy(() => import('./VitalSharingWindow').then(m => ({ default: m.VitalSharingWindow })));
import type { CharacterState } from '../../types';
interface Props {
@@ -18,11 +18,11 @@ interface Props {
socket: WebSocket | null;
}
-export const WindowRenderer: React.FC = ({ characters, chatMessages, nearbyObjects, socket }) => {
+export const WindowRenderer: React.FC = React.memo(({ characters, chatMessages, nearbyObjects, socket }) => {
const { windows } = useWindowManager();
return (
- <>
+
{windows.map(w => {
const charName = w.charName ?? '';
const prefix = w.id.split('-')[0];
@@ -53,6 +53,8 @@ export const WindowRenderer: React.FC = ({ characters, chatMessages, near
return null;
}
})}
- >
+
);
-};
+});
+
+WindowRenderer.displayName = 'WindowRenderer';
diff --git a/frontend/src/hooks/useLiveData.ts b/frontend/src/hooks/useLiveData.ts
index 0e89c864..0e517188 100644
--- a/frontend/src/hooks/useLiveData.ts
+++ b/frontend/src/hooks/useLiveData.ts
@@ -1,4 +1,4 @@
-import { useState, useCallback, useEffect, useRef } from 'react';
+import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { useWebSocket } from './useWebSocket';
import { getLive, getCombatStats, getServerHealth, getTotalRares, getTotalKills } from '../api/endpoints';
import type {
@@ -23,7 +23,10 @@ export function useLiveData(): DashboardState {
const [totalRares, setTotalRares] = useState(0);
const [totalKills, setTotalKills] = useState(0);
const [recentRares, setRecentRares] = useState([]);
- const [chatMessages, setChatMessages] = useState