From 0fa7a821752bd1f206ec8a0dd153a222ff4ce902 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sat, 30 May 2026 19:51:57 +0200 Subject: [PATCH] fix(perf): prevent Canvas double-mount on /site redirect HomePage used to mount the Canvas before its effect fired the redirect to /site, then unmount it as soon as the route changed. That left the WebGL context torn down mid-load with GLTF requests still in flight, which on slow GPUs ended in a 'Context Lost' and a stuck 1 FPS render once the user came back from /site. The fix is a synchronous cookie check after all hooks: if the user has not visited /site today we return null and let the redirect happen without ever creating a GL context. Also drops the GameMap 'lite map skipped' log from warn to info: it is an expected lite-loading path, not a problem worth a yellow warning. --- src/pages/page.tsx | 22 ++++++++++++++-------- src/world/GameMap.tsx | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pages/page.tsx b/src/pages/page.tsx index 45611d7..b821aec 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -19,17 +19,10 @@ import { hasSiteBeenVisitedToday } from "@/utils/cookies/siteVisitCookie"; import { logger } from "@/utils/core/Logger"; import { World } from "@/world/World"; -export function HomePage(): React.JSX.Element { +export function HomePage(): React.JSX.Element | null { const navigate = useNavigate(); const introStep = useGameStore((state) => state.intro.currentStep); const setIntroStep = useGameStore((state) => state.setIntroStep); - - useEffect(() => { - if (!hasSiteBeenVisitedToday()) { - navigate({ to: "/site", replace: true }); - } - }, [navigate]); - const dialogMessage = useGameStore( (state) => state.missionFlow.dialogMessage, ); @@ -38,6 +31,12 @@ export function HomePage(): React.JSX.Element { INITIAL_SCENE_LOADING_STATE, ); + useEffect(() => { + if (!hasSiteBeenVisitedToday()) { + navigate({ to: "/site", replace: true }); + } + }, [navigate]); + useEffect(() => { if (!dialogMessage) return undefined; @@ -98,6 +97,13 @@ export function HomePage(): React.JSX.Element { [], ); + // Don't mount the Canvas until we know we will not redirect to /site. + // Without this guard the Canvas would mount, the effect above would fire + // navigate, and the Canvas would unmount mid-load — leaking GLTF requests + // and a WebGL context. The synchronous cookie check happens here AFTER + // all hooks (rules of hooks) but BEFORE any expensive render. + if (!hasSiteBeenVisitedToday()) return null; + const renderIntroOverlay = () => { switch (introStep) { case "video": diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index aee0ee8..33048e5 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -175,7 +175,7 @@ export function GameMap({ sceneData.mapNodes.length - visibleMapNodes.length; if (skippedMapNodeCount > 0) { - logger.warn("GameMap", "Lite map skipped heavy map nodes", { + logger.info("GameMap", "Lite map skipped heavy map nodes", { skippedMapNodeCount, }); }