/** * MessageList — 消息列表容器 */ import { useEffect, useRef } from 'react'; import { Box, Typography } from '@mui/material'; import { useAgentStore } from '@renderer/stores/agent-store'; import { MessageItem } from './MessageItem'; import { StreamingIndicator } from './StreamingIndicator'; export function MessageList(): React.JSX.Element { const messages = useAgentStore((s) => s.messages); const isStreaming = useAgentStore((s) => s.isStreaming); const messagesEndRef = useRef(null); // F2: 滚动节流优化 // 问题:原实现用 setTimeout(80) debounce + 'smooth',高频 delta 时 trailing 永不触发, // 且 'smooth' 在长列表上触发主线程布局动画,加剧卡顿。 // 方案: // - 流式时:100ms 节流 + trailing 兜底 + 'auto' 行为(同步布局,无动画占主线程) // - 非流式时:立即 'smooth' 滚动(新消息发送/接收完成) // - 用 rAF 同步到下一帧,与 React 渲染合并,避免一帧内多次布局 const lastScrollRef = useRef(0); const trailingTimerRef = useRef(null); const rafRef = useRef(null); useEffect(() => { const now = performance.now(); const elapsed = now - lastScrollRef.current; // 调度一次 rAF 滚动(自动取消上一次挂起的 rAF) const doScroll = (behavior: ScrollBehavior) => { lastScrollRef.current = performance.now(); if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(() => { rafRef.current = null; messagesEndRef.current?.scrollIntoView({ behavior }); }); }; // 非流式或首次进入:立即平滑滚动(新消息出现) if (!isStreaming || lastScrollRef.current === 0) { if (trailingTimerRef.current !== null) { clearTimeout(trailingTimerRef.current); trailingTimerRef.current = null; } doScroll('smooth'); return; } // 流式中:100ms 节流 + trailing 兜底 if (elapsed >= 100) { // 已过节流窗口,立即执行 if (trailingTimerRef.current !== null) { clearTimeout(trailingTimerRef.current); trailingTimerRef.current = null; } doScroll('auto'); } else if (trailingTimerRef.current === null) { // 在节流窗口内,安排 trailing 滚动(保证最后一次 delta 后到底) const remaining = 100 - elapsed; trailingTimerRef.current = window.setTimeout(() => { trailingTimerRef.current = null; doScroll('auto'); }, remaining); } }, [messages, isStreaming]); // 组件卸载时清理所有挂起的 timer 和 rAF useEffect(() => { return () => { if (trailingTimerRef.current !== null) { clearTimeout(trailingTimerRef.current); trailingTimerRef.current = null; } if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } }; }, []); if (messages.length === 0 && !isStreaming) { return ( MetonaAI Desktop 生产级通用 AI Agent 智能体桌面应用 Agent 就绪 · 选择一个 Provider 开始对话 ); } return ( // F12: GPU 加速 — transform: translateZ(0) 将滚动容器提升为合成层 // 滚动时由合成器线程处理,避免主线程重绘叠加卡顿 // 风险评估:已确认 chat 目录内无 position: fixed 元素; // ContextMenu 用 MUI Portal 渲染到 document.body,不受 transform 影响 {messages.map((msg, i) => ( // F4: content-visibility 准虚拟滚动 // 浏览器原生支持,不可见区域跳过布局和绘制(DOM 节点保留但渲染开销 O(1))。 // 配合 F1 memo(跳过 re-render)+ F3 流式纯文本,历史消息开销降至最低。 // contain-intrinsic-size 提供估算高度,避免滚动时高度抖动。 // 注:如后续需真正虚拟滚动(移除 DOM 节点),可升级到 react-virtuoso。 ))}
); }