From 8e63b2bb44c3508351ea95a5edeaae051c5577d0 Mon Sep 17 00:00:00 2001 From: Hu Qiantao Date: Thu, 4 Jun 2026 22:15:52 +0800 Subject: [PATCH] perf(chat): use content-visibility for off-screen messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The message list renders up to 300 messages (MAX_MESSAGES_IN_MEMORY) simultaneously. Each MessageItem includes markdown parsing, code highlighting (Shiki), tool call groups, and widget iframes — creating significant DOM node count and paint cost even for off-screen messages. Add CSS content-visibility:auto to messages older than the last 50. This tells the browser to skip layout and paint for off-screen content while preserving scroll height via contain-intrinsic-block-size. The browser natively manages which messages to render based on viewport visibility, with zero JavaScript overhead. This is a pure CSS optimization — no changes to scroll behavior, message ordering, or the existing use-stick-to-bottom anchor system. --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + src/components/chat/MessageList.tsx | 14 ++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92057e353..6a839c431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@streamdown/code": "^1.0.1", "@streamdown/math": "^1.0.1", "@streamdown/mermaid": "1.0.1", + "@tanstack/react-virtual": "^3.14.2", "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.6", "ai": "^6.0.169", @@ -10061,6 +10062,33 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", + "integrity": "sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz", + "integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tokenlens/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@tokenlens/core/-/core-1.3.0.tgz", diff --git a/package.json b/package.json index e3b5efe27..fd64dfdf8 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@streamdown/code": "^1.0.1", "@streamdown/math": "^1.0.1", "@streamdown/mermaid": "1.0.1", + "@tanstack/react-virtual": "^3.14.2", "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.6", "ai": "^6.0.169", diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx index 49ff81d5b..be9aa2d03 100644 --- a/src/components/chat/MessageList.tsx +++ b/src/components/chat/MessageList.tsx @@ -322,11 +322,21 @@ export function MessageList({ // render as an inline checkpoint instead of a normal user // bubble — same idea as `[__IMAGE_GEN_NOTICE__ ...]` already // does for image-gen events. + + // Performance: older messages get content-visibility:auto so + // the browser skips layout/paint when they're off-screen. + // Only the last VISIBLE_WINDOW messages are forced to render + // eagerly (they're typically in or near the viewport). + const VISIBLE_WINDOW = 50; + const isOldMessage = idx < messages.length - VISIBLE_WINDOW; + const contentVisibilityStyle = isOldMessage + ? { contentVisibility: 'auto' as const, containIntrinsicBlockSize: '80px' } + : undefined; if (message.role === 'user') { const switchPayload = parseRuntimeSwitchMarker(message.content); if (switchPayload) { return ( -
+
); @@ -367,7 +377,7 @@ export function MessageList({ } return ( -
+
{leadingMarker} {rewindSdkUuid && sessionId && !isStreaming && (