使用 Codex 开发 React、Next.js 或后台管理系统时,列表页面几乎是最常见的功能之一。

数据只有几十条时,下面这样的代码通常没有任何问题:

{users.map(user => (
  <UserRow
    key={user.id}
    user={user}
  />
))}

但当列表逐渐增长到:

500条
2000条
10000条

页面就可能开始出现:

  • 首次进入页面明显变慢;

  • 滚动时掉帧;

  • 输入搜索框时整个页面卡顿;

  • 勾选一行,几千行一起重新渲染;

  • 浏览器内存持续上升;

  • Codex 不断增加 useMemouseCallback,效果却不明显。

这类问题很多时候并不是 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 与无效重渲染带来的性能消耗。

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐