From e644def14154495bd05f99f3a3c3fd010f415a45 Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Sun, 5 Apr 2026 06:58:53 -0400 Subject: [PATCH] Fix mobile performance degradation during typing and after image upload Memoize blob URL creation to prevent leak on every keystroke, wrap MessageInput in React.memo with stable useCallback props, remove expensive backdrop-blur-sm from chat footer, and use instant scroll during streaming to avoid queuing smooth scroll animations. Co-Authored-By: Claude Opus 4.6 --- raggr-frontend/src/components/ChatScreen.tsx | 33 +++++++++++-------- .../src/components/MessageInput.tsx | 20 ++++++++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/raggr-frontend/src/components/ChatScreen.tsx b/raggr-frontend/src/components/ChatScreen.tsx index 57ac902..190410e 100644 --- a/raggr-frontend/src/components/ChatScreen.tsx +++ b/raggr-frontend/src/components/ChatScreen.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import { LogOut, Shield, PanelLeftClose, PanelLeftOpen, Menu, X } from "lucide-react"; import { conversationService } from "../api/conversationService"; import { userService } from "../api/userService"; @@ -63,9 +63,13 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { const abortControllerRef = useRef(null); const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"]; - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + const scrollToBottom = useCallback(() => { + requestAnimationFrame(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: isLoading ? "instant" : "smooth", + }); + }); + }, [isLoading]); useEffect(() => { isMountedRef.current = true; @@ -130,7 +134,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { load(); }, [selectedConversation?.id]); - const handleQuestionSubmit = async () => { + const handleQuestionSubmit = useCallback(async () => { if ((!query.trim() && !pendingImage) || isLoading) return; let activeConversation = selectedConversation; @@ -214,19 +218,22 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { if (isMountedRef.current) setIsLoading(false); abortControllerRef.current = null; } - }; + }, [query, pendingImage, isLoading, selectedConversation, simbaMode, messages, setAuthenticated]); - const handleQueryChange = (event: React.ChangeEvent) => { + const handleQueryChange = useCallback((event: React.ChangeEvent) => { setQuery(event.target.value); - }; + }, []); - const handleKeyDown = (event: React.ChangeEvent) => { + const handleKeyDown = useCallback((event: React.ChangeEvent) => { const kev = event as unknown as React.KeyboardEvent; if (kev.key === "Enter" && !kev.shiftKey) { kev.preventDefault(); handleQuestionSubmit(); } - }; + }, [handleQuestionSubmit]); + + const handleImageSelect = useCallback((file: File) => setPendingImage(file), []); + const handleClearImage = useCallback(() => setPendingImage(null), []); const handleLogout = () => { localStorage.removeItem("access_token"); @@ -380,8 +387,8 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { setSimbaMode={setSimbaMode} isLoading={isLoading} pendingImage={pendingImage} - onImageSelect={(file) => setPendingImage(file)} - onClearImage={() => setPendingImage(null)} + onImageSelect={handleImageSelect} + onClearImage={handleClearImage} /> @@ -416,7 +423,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { -