Skip to content

conversations组件items过多时(超过100),点击切换卡顿 #1871

Open

Description

@friday110

重现步骤

示例代码:
'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

  1. dosubot commented on Apr 19, 2026

    @dosubot

    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:

    1. No memoization — ConversationsItem is a plain functional component without React.memo [1]. When activeKey changes, every item re-renders, even items whose active prop stays false → false.

    2. 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.

    3. Virtual list library available but unused — @rc-component/virtual-list is already in the project dependencies [3], but only used in test mocks [4]. It could be integrated into Conversations to only render visible items.

    Potential fixes:

    • Wrap ConversationsItem (and GroupTitle) with React.memo — this alone would prevent most items from re-rendering on activeKey change and should significantly reduce the click handler time.
    • Integrate @rc-component/virtual-list for virtual scrolling to keep the DOM lightweight when item counts grow large.
    • Memoize callback props passed to items with useCallback to complement React.memo.

    As a workaround until this is fixed upstream, you can try:

    • Keeping the rendered list smaller by implementing pagination or lazy rendering yourself.
    • Using React.startTransition around setActiveKey to 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.

    Leave Feedback Learn about x with Dosu Add Dosu to your team

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions