批量操作性能优化实战:从原理到落地的完整方案

UX-东景 交互 阅读 2,538
赞 35 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月做的一个后台管理项目,用户要能批量操作表格里的数据:勾选几条记录,然后一键删除、导出、或者批量修改状态。一开始觉得这不就是加个 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?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论