perf: Add performance optimizations and Tanuki avatar

Frontend:
- Add memoization to MessageList with custom comparison function
- Implement incremental caching for processedMessages to avoid O(n) rebuilds during streaming
- Wrap Message component with memo()
- Add better error handling for file uploads in SessionContext

Backend:
- Improve upload error handling with proper response checks

Infrastructure:
- Add client_max_body_size 100m to nginx for file uploads
- Add Tanuki avatar (optimized 256x256, 77KB)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 15:34:46 +01:00
parent 165a7729a1
commit 580273bed0
8 changed files with 307 additions and 176 deletions

View File

@@ -119,8 +119,28 @@ function SystemHints({ reminders, inline = false }) {
);
}
// Not using memo here - needs to re-render when HostContext updates
export function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
// Custom comparison for MessageList - only re-render when these specific things change
const messageListPropsAreEqual = (prev, next) => {
// Check if messages array length changed
if (prev.messages.length !== next.messages.length) return false;
// Check if last message content changed (for streaming)
if (prev.messages.length > 0 && next.messages.length > 0) {
const prevLast = prev.messages[prev.messages.length - 1];
const nextLast = next.messages[next.messages.length - 1];
if (prevLast.content !== nextLast.content) return false;
if (prevLast.type !== nextLast.type) return false;
}
// Check other props
return (
prev.isProcessing === next.isProcessing &&
prev.onSendMessage === next.onSendMessage &&
prev.hostId === next.hostId
);
};
export const MessageList = memo(function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
const { hosts } = useHosts();
const hostConfig = hosts[hostId] || null;
const containerRef = useRef(null);
@@ -174,8 +194,35 @@ export function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
setNewMessageCount(0);
}, []);
// Cache for incremental updates - avoid full rebuild on streaming content changes
const processedCacheRef = useRef({ messages: [], result: [], toolResultMap: new Map() });
// Preprocess messages to pair tool_use with tool_result
// Optimized: only rebuild when structure changes, not content
const processedMessages = useMemo(() => {
const cache = processedCacheRef.current;
// Fast path: if only last message content changed (streaming), return cached result
if (cache.messages.length === messages.length && messages.length > 0) {
const lastCached = cache.messages[cache.messages.length - 1];
const lastCurrent = messages[messages.length - 1];
// Check if structure is same (only content might have changed)
if (lastCached.type === lastCurrent.type &&
lastCached.timestamp === lastCurrent.timestamp &&
lastCached.toolUseId === lastCurrent.toolUseId) {
// Content update only - update the cached result's last item content
if (cache.result.length > 0 && lastCurrent.type === 'assistant') {
cache.result[cache.result.length - 1] = {
...cache.result[cache.result.length - 1],
content: lastCurrent.content
};
}
return cache.result;
}
}
// Full rebuild needed
const result = [];
const toolResultMap = new Map();
@@ -207,6 +254,11 @@ export function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
}
});
// Update cache
cache.messages = messages;
cache.result = result;
cache.toolResultMap = toolResultMap;
return result;
}, [messages]);
@@ -265,9 +317,10 @@ export function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
)}
</div>
);
}
}, messageListPropsAreEqual);
function Message({ message, onSendMessage, hostConfig }) {
// Memoize Message component to prevent re-renders during streaming
const Message = memo(function Message({ message, onSendMessage, hostConfig }) {
const { type, content, tool, input, timestamp, toolUseId, attachments } = message;
const { user } = useAuth();
@@ -434,7 +487,7 @@ function Message({ message, onSendMessage, hostConfig }) {
};
return <div>{renderContent()}</div>;
}
});
// Tool configuration with icons, colors, and display logic
const TOOL_CONFIG = {