Repository navigation
conversations组件items过多时(超过100),点击切换卡顿 #1871
Description
重现步骤
示例代码:
'use client'
import { useState, useCallback, useMemo } from 'react'
import { Conversations, type ConversationItemType } from '@ant-design/x'
import { MessageOutlined, UserOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { Modal, Input, message, Button } from 'antd'
import type { MenuProps } from 'antd'
const generateMockData = (start: number, count: number): ConversationItemType[] => {
const now = Date.now()
const day = 24 * 60 * 60 * 1000
const items: ConversationItemType[] = []
for (let i = start; i < start + count; i++) {
const isToday = i <= 150
const isYesterday = i > 150 && i <= 300
items.push({
key: String(i),
label: `会话 ${i}`,
timestamp: isToday
? now - (i - 1) * 3600000
: isYesterday
? now - day - (i - 151) * 3600000
: now - (2 * day) - (i - 301) * 3600000,
group: isToday ? '今天' : isYesterday ? '昨天' : '更早',
icon: i % 3 === 0 ? <MessageOutlined /> : <UserOutlined />,
})
}
return items
}
const PAGE_SIZE = 50
export default function Home() {
const [items, setItems] = useState(() => generateMockData(1, PAGE_SIZE))
const [activeKey, setActiveKey] = useState('1')
const [renameValue, setRenameValue] = useState('')
const [isModalOpen, setIsModalOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const loadMore = useCallback(async () => {
setLoading(true)
await new Promise((r) => setTimeout(r, 300))
const nextItems = generateMockData(items.length + 1, PAGE_SIZE)
if (nextItems.length > 0) {
setItems((prev) => [...prev, ...nextItems])
if (items.length >= 500) {
setHasMore(false)
}
} else {
setHasMore(false)
}
setLoading(false)
}, [items.length])
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
if (!activeKey) return
if (key === 'delete') {
setItems((prev) => prev.filter((item) => item.key !== activeKey))
message.success('删除成功')
} else if (key === 'rename') {
setRenameValue(
items.find((item) => item.key === activeKey)?.label?.toString() || ''
)
setIsModalOpen(true)
}
}
const menuItems: MenuProps['items'] = useMemo(() => [
{
key: 'rename',
icon: ,
label: '重命名',
},
{
key: 'delete',
icon: ,
label: '删除',
danger: true,
},
], [])
const handleRenameOk = () => {
if (activeKey && renameValue.trim()) {
setItems((prev) =>
prev.map((item) =>
item.key === activeKey ? { ...item, label: renameValue.trim() } : item
)
)
message.success('重命名成功')
setIsModalOpen(false)
}
}
return (
<div style={{ height: '100vh', width: 320, borderRight: '1px solid #f0f0f0', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto' }}>
<Conversations
items={items}
activeKey={activeKey}
onActiveChange={setActiveKey}
groupable
shortcutKeys={{
creation: ['Ctrl', 'q'] as any,
}}
menu={{
items: menuItems,
onClick: handleMenuClick,
}}
/>
{hasMore && (
<div style={{ padding: 12, borderTop: '1px solid #f0f0f0' }}>
加载更多 ({items.length}/500)
)}
<Modal
title="重命名会话"
open={isModalOpen}
onOk={handleRenameOk}
onCancel={() => setIsModalOpen(false)}
>
<Input
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
placeholder="请输入新名称"
/>
)
}
当前行为
当item数量过大时,比如500,界面切换item有卡顿感,打开f12 console中日志:[Violation] 'click' handler took ms
[Violation] 'click' handler took 169ms
react-dom.development.js:8016 [Violation] 'click' handler took 195ms
react-dom.development.js:8016 [Violation] 'click' handler took 215ms
react-dom.development.js:8016 [Violation] 'click' handler took 244ms
react-dom.development.js:8016 [Violation] 'click' handler took 270ms
react-dom.development.js:8016 [Violation] 'click' handler took 274ms
react-dom.development.js:8016 [Violation] 'click' handler took 268ms
react-dom.development.js:8016 [Violation] 'click' handler took 264ms
react-dom.development.js:8016 [Violation] 'click' handler took 262ms
预期行为
No response
上下文
No response
版本
2.5.0
您在哪些浏览器上遇到了这个问题?
Chrome
Activity
dosubot commentedon Apr 19, 2026
Hi @friday110! I'm Dosu and I’m helping the x team.
This performance issue is reproducible and the root cause is clear from the source code:
No memoization —
ConversationsItemis a plain functional component withoutReact.memo[1]. WhenactiveKeychanges, every item re-renders, even items whoseactiveprop staysfalse→false.No virtualization — The component renders all items via a simple
.map()[2], so with 500 items, all 500 DOM nodes exist and all 500 components go through React reconciliation on each click.Virtual list library available but unused —
@rc-component/virtual-listis already in the project dependencies [3], but only used in test mocks [4]. It could be integrated intoConversationsto only render visible items.Potential fixes:
ConversationsItem(andGroupTitle) withReact.memo— this alone would prevent most items from re-rendering onactiveKeychange and should significantly reduce the click handler time.@rc-component/virtual-listfor virtual scrolling to keep the DOM lightweight when item counts grow large.useCallbackto complementReact.memo.As a workaround until this is fixed upstream, you can try:
React.startTransitionaroundsetActiveKeyto make the re-render non-blocking (won't reduce total work, but keeps the UI responsive).To reply, just mention @dosu.
Share context across your team and agents. Try Dosu.