批量操作性能优化实战:从原理到落地的完整方案
项目初期的技术选型
上个月做的一个后台管理项目,用户要能批量操作表格里的数据:勾选几条记录,然后一键删除、导出、或者批量修改状态。一开始觉得这不就是加个 checkbox,维护个选中数组的事嘛,前端 CRUD 谁不会?但真做起来才发现,坑比想象中多得多。
我们用的是 React + Ant Design,表格组件是 Table。官方文档里有 batch selection 的 demo,看起来挺简单:开个 rowSelection,绑个 selectedRowKeys,搞定。但实际业务复杂得多——比如有些行不能选(状态为“已处理”的不能删),有些操作需要二次确认,还有性能问题……这些文档里可没提。
最大的坑:性能问题
最开始我图省事,直接在 state 里存 selectedRowKeys,每次 onChange 就 set 一下。结果测试同学一上来就勾了 5000 条数据,页面直接卡死。Chrome DevTools 一开,render 时间飙到 800ms+,滚动都卡成幻灯片。
折腾了半天发现,问题出在两个地方:
- Table 组件每次 selectedRowKeys 变化都会重新渲染所有行(哪怕只改了一行)
- 我的 onChange 回调里还做了额外的过滤和校验,比如判断哪些行能被当前操作选中
后来我试了 memoize,但效果不大。最后灵机一动:既然用户一次最多操作几百条(产品说超过 500 条就要弹警告),那我就别实时同步全部选中项,而是用一个 Set 来缓存,只在真正触发操作时才去读取。这样 Table 的 selectedRowKeys 只在必要时更新,避免频繁重渲染。
核心代码长这样:
const [selectedKeys, setSelectedKeys] = useState(new Set());
const [tableSelectedKeys, setTableSelectedKeys] = useState([]);
// 表格的 rowSelection 配置
const rowSelection = {
selectedRowKeys: tableSelectedKeys,
onChange: (keys) => {
// 用户手动勾选/取消
const newSet = new Set(keys);
selectedKeysRef.current = newSet; // 用 ref 缓存,避免 setState 触发重渲染
setTableSelectedKeys(keys); // 只更新 Table 需要的部分
},
getCheckboxProps: (record) => ({
disabled: record.status === 'processed', // 禁用某些行
}),
};
// 真正执行批量操作时,从 ref 里拿最新数据
const handleBatchDelete = () => {
const keys = Array.from(selectedKeysRef.current);
if (keys.length === 0) return;
// ... 调用 API
};
另一个头疼的问题:状态同步
除了性能,还有个更隐蔽的问题:当用户执行完批量操作后,比如删了 100 条,表格要自动刷新,这时候选中状态怎么处理?
最开始我直接清空 selectedKeys,但产品说:“如果删了部分,剩下的还应该保持选中”。行吧,那就得在刷新后重新计算选中项。但新数据和旧数据 ID 不一定连续,搞不好就错位。
我最后的方案是:把选中项的 ID 存在 useRef 里,刷新后遍历新数据,保留那些还在新数据中的 ID。虽然有点啰嗦,但胜在稳定:
const refreshTable = async () => {
const newData = await fetch('/api/list');
setData(newData);
// 保留仍然存在的选中项
const stillExists = Array.from(selectedKeysRef.current).filter(id =>
newData.some(item => item.id === id)
);
selectedKeysRef.current = new Set(stillExists);
setTableSelectedKeys(stillExists);
};
这里注意我踩过好几次坑:一开始用 state 存 selectedKeys,结果异步刷新时状态已经 stale 了,必须用 ref 才能拿到最新值。
核心代码就这几行
其实整个批量操作的核心逻辑并不复杂,关键是要把“UI 显示”和“真实选中状态”分开管理。下面是我最终封装的简化版 hook,亲测有效:
import { useState, useRef } from 'react';
function useBatchSelection(initialData = []) {
const selectedRef = useRef(new Set());
const [displayKeys, setDisplayKeys] = useState([]);
const toggleSelection = (keys) => {
selectedRef.current = new Set(keys);
setDisplayKeys(keys);
};
const getSelected = () => Array.from(selectedRef.current);
const clearSelection = () => {
selectedRef.current = new Set();
setDisplayKeys([]);
};
const syncWithNewData = (newData, idKey = 'id') => {
const validIds = new Set(newData.map(item => item[idKey]));
const filtered = Array.from(selectedRef.current).filter(id => validIds.has(id));
selectedRef.current = new Set(filtered);
setDisplayKeys(filtered);
};
return {
displayKeys,
toggleSelection,
getSelected,
clearSelection,
syncWithNewData,
};
}
用的时候:
const {
displayKeys,
toggleSelection,
getSelected,
syncWithNewData
} = useBatchSelection();
// Table 的 rowSelection
const rowSelection = {
selectedRowKeys: displayKeys,
onChange: toggleSelection,
getCheckboxProps: (record) => ({ disabled: record.disabled }),
};
// 刷新后
const handleRefresh = (newData) => {
setData(newData);
syncWithNewData(newData);
};
回顾与反思
这套方案上线后,基本没再收到性能相关的反馈。不过还是有两个小问题没彻底解决:
- 如果用户勾选了 5000 条,虽然 UI 不卡了,但点击“删除”时弹窗确认还是会卡一下(因为要遍历 Set 生成提示文案)。后来产品妥协了,超过 200 条就不显示具体数量,只写“大量数据”
- 在极少数情况下(比如网络慢,刷新中途又操作),选中状态会错乱。但概率太低,优先级不高,就先放着了
总的来说,这次让我明白:看似简单的“批量操作”,背后藏着不少细节。尤其是状态管理和性能优化,不能想当然。用 ref 缓存中间状态、分离 UI 与逻辑、控制重渲染范围,这些技巧比炫技的 hooks 更实用。
如果你也在做类似功能,建议早点考虑大数据量场景,别等 QA 拿 5000 条数据砸你脸上才想起来优化。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如有没有办法用 immutable.js 或者更巧妙的 diff 算法来优化 syncWithNewData?

暂无评论