From f3a8096ff6a29c033972bc4187ce58d42b3645fb Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 26 Mar 2026 22:34:34 +0200 Subject: [PATCH] perf(site): move useSpeechRecognition into compiled path (#23689) The hook lived at src/hooks/, outside the React Compiler scope. It returned a new object literal every render, causing three handler guards and two downstream JSX guards in AgentChatInput (233 cache slots) to always miss. Move the hook to src/pages/AgentsPage/hooks/ where the compiler processes it. The compiler auto-memoizes the return object, so manual useMemo is unnecessary. Also replace ctorRef.current render-time access with a useState lazy initializer. The ref access caused a CompileError that would have prevented compilation. Browser API availability is constant, so useState captures it once. --- .../AgentsPage/components/AgentChatInput.tsx | 2 +- .../hooks/useSpeechRecognition.test.ts | 0 .../AgentsPage}/hooks/useSpeechRecognition.ts | 18 +++++++----------- 3 files changed, 8 insertions(+), 12 deletions(-) rename site/src/{ => pages/AgentsPage}/hooks/useSpeechRecognition.test.ts (100%) rename site/src/{ => pages/AgentsPage}/hooks/useSpeechRecognition.ts (93%) diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index 83bf8f4614..ca4fdfd20b 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -57,11 +57,11 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { useSpeechRecognition } from "#/hooks/useSpeechRecognition"; import { cn } from "#/utils/cn"; import { countInvisibleCharacters } from "#/utils/invisibleUnicode"; import { isMobileViewport } from "#/utils/mobile"; import { useOverflowCount } from "../hooks/useOverflowCount"; +import { useSpeechRecognition } from "../hooks/useSpeechRecognition"; import { fetchTextAttachmentContent, formatTextAttachmentPreview, diff --git a/site/src/hooks/useSpeechRecognition.test.ts b/site/src/pages/AgentsPage/hooks/useSpeechRecognition.test.ts similarity index 100% rename from site/src/hooks/useSpeechRecognition.test.ts rename to site/src/pages/AgentsPage/hooks/useSpeechRecognition.test.ts diff --git a/site/src/hooks/useSpeechRecognition.ts b/site/src/pages/AgentsPage/hooks/useSpeechRecognition.ts similarity index 93% rename from site/src/hooks/useSpeechRecognition.ts rename to site/src/pages/AgentsPage/hooks/useSpeechRecognition.ts index d8b89906d0..2ad0f06e1e 100644 --- a/site/src/hooks/useSpeechRecognition.ts +++ b/site/src/pages/AgentsPage/hooks/useSpeechRecognition.ts @@ -85,17 +85,13 @@ export function useSpeechRecognition(): { const [error, setError] = useState(null); const recognitionRef = useRef(null); - // Cache the constructor lookup once per hook instance so we don't hit - // the window property on every render. - const ctorRef = useRef( - getSpeechRecognitionCtor(), - ); - - const isSupported = ctorRef.current !== undefined; + // Browser API availability is constant for the lifetime of the + // page, so a lazy state initializer captures it once. + const [ctorSnapshot] = useState(getSpeechRecognitionCtor); + const isSupported = ctorSnapshot !== undefined; const start = useCallback(() => { - const Ctor = ctorRef.current; - if (!Ctor) { + if (!ctorSnapshot) { return; } @@ -107,7 +103,7 @@ export function useSpeechRecognition(): { setError(null); - const recognition = new Ctor(); + const recognition = new ctorSnapshot(); recognition.lang = navigator.language; recognition.continuous = true; recognition.interimResults = true; @@ -147,7 +143,7 @@ export function useSpeechRecognition(): { setTranscript(""); setIsRecording(true); recognition.start(); - }, []); + }, [ctorSnapshot]); const stop = useCallback(() => { // stop() lets the browser deliver any remaining final results