Codex写React大列表为什么数据一多就卡?用虚拟列表减少无效渲染
使用 Codex 开发 React、Next.js 或后台管理系统时,列表页面几乎是最常见的功能之一。
数据只有几十条时,下面这样的代码通常没有任何问题:
{users.map(user => (
<UserRow
key={user.id}
user={user}
/>
))}
但当列表逐渐增长到:
500条
2000条
10000条
页面就可能开始出现:
-
首次进入页面明显变慢;
-
滚动时掉帧;
-
输入搜索框时整个页面卡顿;
-
勾选一行,几千行一起重新渲染;
-
浏览器内存持续上升;
-
Codex 不断增加
useMemo和useCallback,效果却不明显。
这类问题很多时候并不是 React 本身性能差,而是:
页面一次创建和维护了太多实际看不到的 DOM 节点。
一、10000条数据到底意味着什么?
假设每一行:
<tr>
<td>姓名</td>
<td>邮箱</td>
<td>状态</td>
<td>时间</td>
<td>操作按钮</td>
</tr>
10000行意味着至少:
10000个tr
+
50000个td
+
按钮、图标、文本节点
浏览器需要同时维护大量:
DOM
布局计算
样式计算
事件
React节点
即使用户屏幕实际只能看到:
20—30行
其余几千行仍然存在于页面中。
所以优化大列表最重要的问题不是:
怎样让10000行渲染得更快?
而是:
为什么要同时渲染10000行?
二、什么是虚拟列表?
Virtual List,也叫虚拟滚动。
核心思路是:
数据有10000条
↓
屏幕只显示20条
↓
真正创建的DOM只保留几十条
用户滚动时,再动态替换当前可见区域的数据。
例如:
当前滚动位置:
第5000条附近
实际DOM:
4990—5030
而不是把:
1—10000
全部放进 DOM。
这样无论后端返回:
1000
10000
甚至更多
页面实际维护的节点数量都相对稳定。
三、可以使用成熟的虚拟列表库
React 项目可以评估:
react-window
TanStack Virtual
react-virtualized
例如使用类似:
<Virtualizer
count={users.length}
estimateSize={() => 48}
/>
然后只渲染可见元素。
Codex 在处理大列表性能问题时,应该先判断:
是不是DOM数量本身太大?
而不是一上来给所有组件增加 React.memo()。
如果页面上已经存在10000行 DOM,减少几次函数创建通常解决不了根本问题。
四、稳定的key非常重要
错误写法:
users.map((user, index) => (
<UserRow
key={index}
user={user}
/>
))
如果列表发生:
排序
插入
删除
筛选
index对应的数据位置会变化。
React可能错误复用节点。
例如原来:
0 → Tom
1 → Jerry
2 → Alice
删除 Tom:
0 → Jerry
1 → Alice
React看到的 Key 仍然是:
0
1
很容易导致:
-
输入框状态错位;
-
Checkbox勾选错误;
-
行组件不必要重建。
更推荐:
key={user.id}
Key应该代表:
这一条业务数据自己的稳定身份。
五、为什么点一行会让整个列表重新渲染?
例如:
const [selectedId, setSelectedId] =
useState<string | null>(null);
return users.map(user => (
<UserRow
user={user}
selected={
user.id === selectedId
}
onClick={() =>
setSelectedId(user.id)
}
/>
));
selectedId变化以后,父组件重新执行。
所有:
UserRow
都会再次进入渲染流程。
几十行问题不大。
几千行就可能明显卡顿。
这时候可以评估:
const UserRow =
React.memo(function UserRow(props) {
// ...
});
让 Props 没变化的行尽量跳过重复渲染。
六、React.memo不是万能加速按钮
如果父组件每次都传入新的对象:
<UserRow
user={{
id: user.id,
name: user.name
}}
/>
即使内容相同,每次渲染都会创建:
新的对象引用
React.memo 仍然可能认为 Props 已发生变化。
类似:
style={{
height: 48
}}
或者:
options={[
"edit",
"delete"
]}
都会创建新对象。
所以使用 memo 时需要同时关注:
Props引用是否稳定。
七、回调函数也可能让memo失效
例如:
<UserRow
user={user}
onEdit={() => editUser(user.id)}
/>
父组件每次重新渲染都会生成新的:
onEdit函数
于是 UserRow 的 Props 发生变化。
可以根据实际情况使用:
const handleEdit =
useCallback(
(id: string) => {
editUser(id);
},
[]
);
然后:
<UserRow
user={user}
onEdit={handleEdit}
/>
但不要为了“优化”把项目里所有函数都套 useCallback。
只有当:
函数引用变化
确实导致昂贵子组件重复渲染
时才有明显价值。
八、搜索过滤不要每次输入都处理全部数据
例如:
const filteredUsers =
users.filter(user =>
user.name.includes(keyword)
);
用户输入:
c
co
cod
code
codex
每输入一个字符,都可能重新扫描:
10000条数据
如果过滤逻辑还比较复杂,输入框就会出现卡顿。
可以结合:
debounce
例如用户停止输入:
300ms
后再执行过滤。
如果数据量非常大,更应该考虑:
服务端搜索
而不是把全部数据下载到浏览器后再筛选。
九、useMemo适合昂贵的派生计算
例如:
const sortedUsers =
[...users].sort(compareUsers);
如果父组件因为其他状态更新而频繁渲染,这个排序也会不断执行。
可以:
const sortedUsers =
useMemo(() => {
return [...users].sort(compareUsers);
}, [users, sortType]);
这样只有:
users
或
sortType
变化时重新计算。
但如果数组本身每次都重新创建:
const users =
response.users.map(...);
Memo同样可能频繁失效。
所以要从数据流整体判断。
十、不要一次从接口加载所有数据
另一个常见问题:
数据库有50000条
↓
API一次返回50000条
↓
浏览器保存50000条
↓
React渲染50000条
即使使用虚拟列表,前端仍然需要保存和传输大量数据。
更合理的方案通常是:
分页
Cursor Pagination
Infinite Scroll
例如:
首次加载50条
↓
用户接近底部
↓
再加载50条
前面讲过 Cursor Pagination。
在前端大列表中,它同样非常适合:
无限滚动
消息记录
动态信息流
订单历史
十一、虚拟列表和分页可以同时使用
并不是:
用了分页
就不需要虚拟列表
例如聊天记录可能已经加载:
3000条
即使数据是分页拿到的,页面上如果把3000条 DOM 全部渲染出来,仍然可能变慢。
可以组合:
Cursor Pagination
负责控制数据加载量
Virtual List
负责控制DOM数量
两个优化解决的是不同层面的问题。
十二、图片是列表性能的另一个隐形杀手
商品列表中每一行可能包含图片:
<img src={product.image} />
一次加载:
500张高清图
不仅占用网络,还会增加:
图片解码
内存
GPU
布局
可以使用:
loading="lazy"
或者框架自己的图片懒加载机制。
同时生成合适尺寸的缩略图。
不要在:
80 × 80
的列表卡片中加载:
4000 × 4000
的原图。
十三、避免列表行内部执行复杂计算
错误方式:
function UserRow({ user }) {
const permissions =
calculateComplexPermissions(user);
const stats =
calculateUserStats(user);
return ...
}
列表2000行:
两个复杂函数
×
2000
只要重新渲染一次,就会重新计算大量逻辑。
可以考虑:
后端预计算
Selector
Memo
数据层预处理
让行组件尽量只负责展示。
一个理想的 Row 应该尽量“轻”。
十四、不要在每一行都创建复杂组件树
例如每一行都挂:
Dropdown
Tooltip
Modal
Popover
复杂表单
即使用户根本没有打开这些组件,它们也可能增加初始化成本。
可以改成:
用户点击操作
↓
再创建Popover / Modal
或者列表共用一个全局弹窗:
selectedUserId
↓
打开统一EditModal
而不是:
2000行
=
2000个Modal。
十五、事件监听也不要每一行单独绑定复杂逻辑
例如每个 Row 都:
useEffect(() => {
window.addEventListener(
"resize",
handler
);
return () => {
window.removeEventListener(
"resize",
handler
);
};
}, []);
1000行就可能产生:
1000个window resize listener
这显然不合理。
全局事件应该尽量:
统一监听
↓
通过Context / Store传播结果
而不是每个列表项各自监听一遍。
十六、列表状态不要全部放进父组件
假设父组件同时保存:
当前hover行
当前展开行
Checkbox状态
输入框状态
编辑状态
Loading
任何一个小状态变化都可能让整个列表父组件重新执行。
可以根据业务把状态下沉:
局部UI状态
→ Row内部
真正需要跨列表共享的状态再放在父层或 Store。
状态放得越高,影响的渲染范围通常越大。
十七、React Profiler比“感觉卡”更有价值
遇到页面卡顿时,可以使用 React DevTools Profiler 查看:
哪个组件重新渲染最多?
一次Commit用了多久?
为什么这个组件重新渲染?
例如发现:
UserTable
35ms
UserRow × 2000
420ms
那问题就非常明显。
不要只告诉 Codex:
页面有点卡,帮我优化。
更有效的是提供:
Profiler结果
组件数量
数据量
一次更新耗时
让优化有明确目标。
十八、浏览器Performance也要看Long Task
Chrome Performance中,如果出现:
Long Task > 50ms
通常说明主线程被连续占用。
可以继续检查:
Scripting
Rendering
Painting
到底哪一块占比最高。
例如:
Scripting 600ms
可能是 JavaScript计算。
如果:
Rendering 800ms
可能是大量DOM和布局。
不同原因需要不同优化方式。
十九、让Codex先做渲染地图
遇到大型列表卡顿时,可以先要求:
请先不要修改代码。
分析当前列表:
1. 总数据量是多少;
2. 实际同时创建多少DOM节点;
3. 是否使用虚拟列表;
4. 列表Row是否使用稳定key;
5. 父组件状态变化时多少Row会重新渲染;
6. 是否存在每行复杂计算;
7. 是否存在每行Modal / Tooltip;
8. 是否一次性加载全部数据;
9. 哪些优化最可能带来明显收益。
先确认:
瓶颈到底是数据量、DOM、计算还是网络。
再开始重构。
二十、一个推荐的大列表优化顺序
可以固定为:
确认数据规模
↓
限制接口返回量
↓
分页 / Cursor
↓
虚拟列表
↓
稳定Key
↓
减少Row复杂度
↓
定位重复渲染
↓
再使用memo / useMemo / useCallback
不要反过来:
页面有10000个DOM
↓
疯狂添加useMemo
这样往往只能获得很有限的改善。
二十一、测试不能只使用10条Mock数据
开发环境常见:
mock users = 10
然后认为页面性能正常。
真实生产可能:
2000条
5000条
10000条
建议性能测试至少覆盖:
100条
1000条
5000条
观察:
首次渲染耗时
滚动FPS
搜索响应
内存占用
组件重渲染次数
如果数据量增加10倍,DOM数量却基本保持不变,说明虚拟化已经真正发挥作用。
二十二、把大列表规则写进AGENTS.md
# Frontend Large List规则
- 超过大量列表数据时必须评估Virtual List
- 列表key必须使用稳定业务ID
- 禁止默认使用数组index作为动态列表key
- 大型列表禁止一次性创建全部DOM
- 后端大数据列表必须评估分页或Cursor
- Row组件必须避免无意义复杂计算
- 禁止每一行默认挂载大型Modal或全局事件监听
- React.memo必须结合真实重渲染问题使用
- 性能优化前必须查看Profiler或Performance数据
- 大列表修改后必须使用真实数据规模测试
这样 Codex 后续处理前端列表时,就不会只以:
页面能显示出来
作为完成标准。
二十三、Plus还是Pro?
如果主要使用 Codex 处理:
-
普通 React 页面;
-
单个列表组件;
-
简单分页;
-
少量性能问题;
Plus 通常已经可以覆盖大部分开发需求。
如果长期处理:
-
大型 React / Next.js 仓库;
-
多个复杂表格;
-
大量自定义 Hooks;
-
Profiler 性能分析;
-
多轮重构与测试;
则可以根据实际开发强度评估 Pro。
对于复杂性能优化任务,更连续的上下文更适合同时分析组件树、数据流和多文件依赖。
但无论使用哪个版本,大列表优化最关键的原则都一样:
用户屏幕只能看到几十行,就不要让浏览器同时维护几千行。
总结
Codex 写 React 大列表以后,数据越多页面越卡,很多时候不是 React 渲染能力不足,而是页面同时创建了过量 DOM,并且一次小状态变化触发了大量无意义重渲染。
通过:
Virtual List
分页 / Cursor
稳定Key
React.memo
按需计算
图片懒加载
Profiler
可以把性能从“数据越多越慢”,逐步变成“只处理用户当前真正看得到的内容”。
真正高效的大列表,不是要求浏览器更努力地渲染10000条。
而是:
数据可以有10000条,但页面此刻只需要为眼前这几十条付出渲染成本。
CSDN文章描述
本文介绍 Codex 编写 React 大列表时常见的页面卡顿问题,并通过虚拟列表、稳定 Key、React.memo、分页加载和 Profiler,减少大量 DOM 与无效重渲染带来的性能消耗。
葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。
更多推荐

所有评论(0)