diff --git a/apps/web/src/components/WebSocketConnectionSurface.tsx b/apps/web/src/components/WebSocketConnectionSurface.tsx index b54bd865c8..d3ab679051 100644 --- a/apps/web/src/components/WebSocketConnectionSurface.tsx +++ b/apps/web/src/components/WebSocketConnectionSurface.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from "react"; +import { type ReactNode, useEffect, useEffectEvent, useRef } from "react"; import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState"; import { @@ -147,7 +147,12 @@ export function shouldRestartStalledReconnect( export function WebSocketConnectionCoordinator() { const status = useWsConnectionStatus(); - const [nowMs, setNowMs] = useState(() => Date.now()); + useWebSocketConnectionCoordinatorEffects(status); + + return null; +} + +function useWebSocketConnectionCoordinatorEffects(status: WsConnectionStatus) { const lastForcedReconnectAtRef = useRef(0); const toastIdRef = useRef | null>(null); const toastResetTimerRef = useRef(null); @@ -201,73 +206,7 @@ export function WebSocketConnectionCoordinator() { runReconnect(false); }); - useEffect(() => { - const handleOnline = () => { - triggerAutoReconnect("online"); - }; - const handleFocus = () => { - triggerAutoReconnect("focus"); - }; - - syncBrowserOnlineStatus(); - window.addEventListener("online", handleOnline); - window.addEventListener("offline", syncBrowserOnlineStatus); - window.addEventListener("focus", handleFocus); - return () => { - window.removeEventListener("online", handleOnline); - window.removeEventListener("offline", syncBrowserOnlineStatus); - window.removeEventListener("focus", handleFocus); - }; - }, []); - - useEffect(() => { - if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) { - return; - } - - setNowMs(Date.now()); - const intervalId = window.setInterval(() => { - setNowMs(Date.now()); - }, 1_000); - - return () => { - window.clearInterval(intervalId); - }; - }, [status.nextRetryAt, status.reconnectPhase]); - - useEffect(() => { - if ( - status.reconnectPhase !== "waiting" || - status.nextRetryAt === null || - !status.online || - !status.hasConnected - ) { - return; - } - - const nextRetryAt = status.nextRetryAt; - const timeoutMs = Math.max(0, new Date(nextRetryAt).getTime() - Date.now()) + 1_500; - const timeoutId = window.setTimeout(() => { - const currentStatus = getWsConnectionStatus(); - if (!shouldRestartStalledReconnect(currentStatus, nextRetryAt)) { - return; - } - - runReconnect(false); - }, timeoutMs); - - return () => { - window.clearTimeout(timeoutId); - }; - }, [ - status.hasConnected, - status.nextRetryAt, - status.online, - status.reconnectAttemptCount, - status.reconnectPhase, - ]); - - useEffect(() => { + const syncConnectionToast = useEffectEvent((nowMs: number) => { const uiState = getWsConnectionUiState(status); const previousUiState = previousUiStateRef.current; const previousDisconnectedAt = previousDisconnectedAtRef.current; @@ -365,7 +304,76 @@ export function WebSocketConnectionCoordinator() { previousUiStateRef.current = uiState; previousDisconnectedAtRef.current = status.disconnectedAt; - }, [nowMs, status]); + }); + + useEffect(() => { + const handleOnline = () => { + triggerAutoReconnect("online"); + }; + const handleFocus = () => { + triggerAutoReconnect("focus"); + }; + + syncBrowserOnlineStatus(); + window.addEventListener("online", handleOnline); + window.addEventListener("offline", syncBrowserOnlineStatus); + window.addEventListener("focus", handleFocus); + return () => { + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", syncBrowserOnlineStatus); + window.removeEventListener("focus", handleFocus); + }; + }, []); + + useEffect(() => { + if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) { + return; + } + + const intervalId = window.setInterval(() => { + syncConnectionToast(Date.now()); + }, 1_000); + + return () => { + window.clearInterval(intervalId); + }; + }, [status.nextRetryAt, status.reconnectPhase]); + + useEffect(() => { + if ( + status.reconnectPhase !== "waiting" || + status.nextRetryAt === null || + !status.online || + !status.hasConnected + ) { + return; + } + + const nextRetryAt = status.nextRetryAt; + const timeoutMs = Math.max(0, new Date(nextRetryAt).getTime() - Date.now()) + 1_500; + const timeoutId = window.setTimeout(() => { + const currentStatus = getWsConnectionStatus(); + if (!shouldRestartStalledReconnect(currentStatus, nextRetryAt)) { + return; + } + + runReconnect(false); + }, timeoutMs); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [ + status.hasConnected, + status.nextRetryAt, + status.online, + status.reconnectAttemptCount, + status.reconnectPhase, + ]); + + useEffect(() => { + syncConnectionToast(Date.now()); + }, [status]); useEffect(() => { return () => { @@ -374,8 +382,6 @@ export function WebSocketConnectionCoordinator() { } }; }, []); - - return null; } export function SlowRpcAckToastCoordinator() { diff --git a/docs/performance/react-scan-websocket-coordinator-after.json b/docs/performance/react-scan-websocket-coordinator-after.json new file mode 100644 index 0000000000..30b5372030 --- /dev/null +++ b/docs/performance/react-scan-websocket-coordinator-after.json @@ -0,0 +1,24 @@ +{ + "label": "after", + "capturedAt": "2026-05-11T16:21:51.199Z", + "reactScanEnabled": true, + "webSocketConnectionCoordinatorRenderCount": 2, + "webSocketConnectionCoordinatorUpdateCount": 1, + "totalActualDurationMs": 1.2, + "metrics": [ + { + "id": "WebSocketConnectionCoordinator", + "phase": "mount", + "actualDuration": 1, + "startTime": 790.3999999761581, + "commitTime": 791.6999999880791 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.20000004768371582, + "startTime": 811.3999999761581, + "commitTime": 811.6000000238419 + } + ] +} diff --git a/docs/performance/react-scan-websocket-coordinator-after.webm b/docs/performance/react-scan-websocket-coordinator-after.webm new file mode 100644 index 0000000000..8c7da911c7 Binary files /dev/null and b/docs/performance/react-scan-websocket-coordinator-after.webm differ diff --git a/docs/performance/react-scan-websocket-coordinator-before.json b/docs/performance/react-scan-websocket-coordinator-before.json new file mode 100644 index 0000000000..8fc41c1871 --- /dev/null +++ b/docs/performance/react-scan-websocket-coordinator-before.json @@ -0,0 +1,73 @@ +{ + "label": "before", + "capturedAt": "2026-05-11T16:20:08.109Z", + "reactScanEnabled": true, + "webSocketConnectionCoordinatorRenderCount": 9, + "webSocketConnectionCoordinatorUpdateCount": 8, + "totalActualDurationMs": 2, + "metrics": [ + { + "id": "WebSocketConnectionCoordinator", + "phase": "mount", + "actualDuration": 0.8999999761581421, + "startTime": 665.6000000238419, + "commitTime": 666.8999999761581 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.30000001192092896, + "startTime": 690.6999999880791, + "commitTime": 691.1000000238419 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.19999998807907104, + "startTime": 769.1000000238419, + "commitTime": 769.3999999761581 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.10000002384185791, + "startTime": 1692, + "commitTime": 1692.199999988079 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.09999996423721313, + "startTime": 2691.800000011921, + "commitTime": 2691.899999976158 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.19999998807907104, + "startTime": 3691.800000011921, + "commitTime": 3692 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.10000002384185791, + "startTime": 4691.899999976158, + "commitTime": 4692.100000023842 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0, + "startTime": 5691.899999976158, + "commitTime": 5691.899999976158 + }, + { + "id": "WebSocketConnectionCoordinator", + "phase": "update", + "actualDuration": 0.10000002384185791, + "startTime": 6691.899999976158, + "commitTime": 6692 + } + ] +} diff --git a/docs/performance/react-scan-websocket-coordinator-before.webm b/docs/performance/react-scan-websocket-coordinator-before.webm new file mode 100644 index 0000000000..09d6a44741 Binary files /dev/null and b/docs/performance/react-scan-websocket-coordinator-before.webm differ