长列表渲染卡顿主因是重绘多而非画得多;需用稳定key(如id)、结构收敛、增量更新、可视区渲染及内存回收来提升性能。

长列表渲染卡顿,核心问题不在“画得多”,而在“重绘多”——每次更新都新建节点、丢弃旧节点,触发大量 DOM 创建/销毁和样式计算。从 Diff 算法本质出发,关键不是“比对更快”,而是“让比对有意义”:通过稳定 key、结构收敛、增量更新和按需挂载,让框架能真正复用节点、跳过无关计算、释放离屏内存。
key 必须稳定且唯一,但不能是索引
用数组下标(i)作 key 是性能杀手:插入/删除时后续所有节点 key 都变,Diff 认为“全不同”,强制卸载重建。正确做法是使用业务中不变的标识,如 id 或 uuid。即使数据暂无 id,也应初始化时生成并持久化(例如存入对象字段),避免渲染时每次 new Date().getTime()。
常见误区:
- 后台分页返回的 list 没带 id?前端需在首次加载时补全并缓存
- 临时 mock 数据用 Math.random()?会导致每次 rerender key 都变,节点无法复用
- 列表项含局部状态(如展开/选中)?key 不稳定会清空这些状态,用户体验断裂
结构收敛:让 Diff 能“跳过整块”
React/Vue 的 Diff 默认逐层比对。若列表项组件内部结构频繁变动(比如条件渲染分支太多、内联函数/对象不断新建),即使 key 正确,子树 diff 也会深度进行,失去复用意义。应主动收束结构:
- 提取静态结构为独立组件,用 React.memo / Vue.shallowRef + v-once 锁定不常变的部分
- 避免在 render 中创建新函数或对象:onClick={() => doX(item)} → 提前绑定或用 data-id 事件代理
- 同一层级尽量保持节点类型一致:不要混用 和 渲染同类数据项,Diff 会直接替换而非复用
可视区渲染 + 节点回收双策略
万级列表不滚动时全量挂载,DOM 节点和绑定事件持续占内存。仅靠虚拟滚动(virtual scroll)还不够,需配合显式内存管理:
- 滚动中只保留当前可视区 ±1~2 屏的节点,其余调用 unmountComponentAtNode() 或手动清理事件监听器、定时器、Canvas context
- 对长期离屏的节点(如滚动停驻 5s 后),主动 dispose 其内部资源(如图表实例、视频元素 pause+src='')
- 使用 WeakMap 存储节点关联状态,确保 DOM 卸载后引用自动释放,避免内存泄漏
Diff 前置:用数据变化驱动,而非视图重算
很多卡顿源于“每次 setState 都全量 diff”。应把计算移到 Diff 之外:
- 用 immer / structural sharing 保证更新前后数据引用可比,diff 时快速跳过未变分支
- 对搜索/过滤结果,缓存上一次结果集与当前参数,仅当参数变才重新计算,避免重复 map/filter
- 复杂排序逻辑抽离到 worker 线程处理,主线程只接收有序 ID 列表,渲染层只需按序取数据
不复杂但容易忽略:节点复用的前提是“它值得被复用”,而内存可控的关键是“知道何时该放手”。Diff 算法只是工具,真正的优化藏在数据建模、生命周期设计和资源契约里。










