为什么 Table 这么慢?!

简介: 为什么 Table 这么慢?!
🙋🏻‍♀️编者按:本文作者为 Ant Design Team,与大家聊一聊为什么 Table 这么“慢”,欢迎查阅~

基本每过几个月,市面上就会出现一个新的 Table 组件库。并且拿 Ant Design 的 Table 做一次对比,长此往复。但是有趣的是,往往无论 Table 组件库如何翻新换代。React 下总是难以出现一款让所有人都满意的 Table。今天,我们简单聊聊,为什么 Ant Design 的 Table 这么“慢”。有的是历史问题,而有的则是无奈之举。

万物皆可缓存,吗?

在 v4 初期,我们重构了 Table。alpha 的 Table 通过 memo 极致缓存了所有行列数据,基本上来说你怎么折腾。只要data不变,Table 就不会重新渲染。这很符合大部分场景下的期待,数据没有变当然展示也不用变。但是往往就会有一些 case 会 break 掉这个直觉:

const [count, setCount] = React.useState(0);
const columns = [
  { render: () => count },
];
return <Table columns={columns} {...} />


真实的业务中,有时 Table Cell 不是纯粹的。开发者可以通过render额外处理一些展示。比如这个例子中,我们简单的将render设置为外部的 statecount。这个时候,虽然data没有变化,但是 Table 还是需要跟随count做重新渲染。


这种与外部 state 交互的代码很常见,比如说我希望用户点击某一个单元格的时候它可以编辑。所以那个行列可以编辑我存在 state 里。那么这个时候,Table 就不能假设 data 可以推导出 memo 唯一性。那么,是不是columns相同,且data相同,那么闭包不会更新我们就可以认为数据不会变呢?当然也是不行的。

let globalValue = 0;
const columns = [{
  render: () => globalValue;
}];
export default () => <Table columns={columns} />;


按照useMemo看,columns没有变化。但是render照样可以被改。这个时候,聪明的小伙伴们肯定会想到那我加个和 effect 相似的deps如何?这样如果deps变化了,Table 说明需要重新渲染,反之就不需要。很可惜,业务是变化多端的。当你在处理遗留代码时,很难发现因为自己新加的逻辑没有添加到deps而导致 Table 没有更新。而知识诅咒会使的越聪明的人越难发现这点。他们会更趋向于去阅读源码然后寻找为什么 Table 没有更新。直到找到后,才发现原来这个就存在于文档之上,然后写一篇文章痛斥为什么 Table 要这么过渡设计。


因而在 antd 中,为了让 Table 元素可以 memo。我们提供了一个更原子的shouldCellUpdate方法。它更贴近渲染,而不是根据deps来做决定。这样即便聪明人,也能一眼看出是什么导致 cell 被 memo 了。

万物皆可虚拟,吗?


接着,老生常谈。为什么 Table 不像其他组件一样提供默认的虚拟滚动能力。说真的,我觉得这是 Table 的基础能力。但是它同样也是一个容易埋坑的东东。一个很常见的例子,就是可编辑表格。


在市面上,90% 的 Form 组件都会根据节点自动注册、卸载字段,从而使得开发者不用去关心什么时候字段出现了。它们总是按照预期的收集数据并且提供给开发者。


但是当你支持虚拟表格后,问题便来了。我们知道,虚拟滚动的亮点在于只渲染看得到的部分从而节约大量的节点渲染性能。但是对于 Form 而言,没有渲染的行列也就不会有节点,那么这个字段对于 Form 而言是不存在的。也就是说,当用户辛辛苦苦编辑完一堆内容并提交后。Form 其实只收集了短短几行的数据,其他内容付之东流。(antd 的 Form 可以通过  getFieldsValue(true)收集所有包括卸载的数据,但是这也能收集到开发者真的不想要的数据)


不同于 Tree、Select 之类的组件,他们的虚拟滚动部分更偏向于数据展示,因而基本不用担心开发者在虚拟滚动中加入副作用。Table 更加灵活,因而如果是可编辑表格往往开发者需要做更耦合的实现来支持虚拟化。


除此之外,还有一些常见的问题。比如超长单元格在滚动到之前不知道它的宽度,因而滚动的时候会突然遇到跳跃的情况。又或者干脆固定宽度失去 Table 原生自适应的能力等等。另外,虚拟滚动对于跨行截断单元格的无障碍支持也会有问题。所以在 antd 中,提供了一个 Demo 关于自行实现虚拟化的功能而没有做成内置的方法。


当然,我还是希望 Table 可以支持虚拟化。只是没有在在想到最好的解法下并不适合立刻动手,以免等到有最优解时又成了历史债务。

万物皆可 Break,吗?


在维护大型组件库时,最让人头疼的就是 breaking change。如果你是 antd 的老用户的话,你肯定记得我们有个很蛋疼的 API 叫做feildNames。是的,它拼错了。于是当我们修正为fieldNames后,feildNames仍然保留到整个 major 版本结束。


很多时候,组件库原本的能力是不够的。为此还需要更多的“洞”来支持自定义操作。比如 antd 里常见的xxxRender系列。Table 也是,如果你观察过,你会发现 Table 的“洞”多如牛毛。基本上所有部件都可以自定义。而随着“洞”的增加,一些原本的 API 又显得很不合理。举个例子。Table 的columnsrender方法你可以返回节点。同时你也可以返回额外的节点props:


const columns = [{
  render: () => ({
    children: 'Hello World',
    rowSpan: 2,
    colSpan: 2,
  });
}];


但是后来,我们又提供了一个onCell方法,你也可以这么写:


const columns = [{
  render: () => 'Hello World',
  onCell: () => ({
    rowSpan: 2,
    colSpan: 2,
  });
}];

从开发者角度看,或许这两个差不多。而且原本的render似乎更内聚一些。但是在实际执行时,它们完全不同。render返回 props 会导致子节点必然被执行一次。而onCell可以无关子节点,而只获取 props。后者可以更好的做渲染优化。举一个例子,Hover 行的时候我们会高亮这一行。但是如果这一行是横跨两行的时候,Hover 应该将两行都高亮(而原生 CSS 则很难做到,因而我们需要动态添加className来实现这个效果):



而无论在动态添加className还是获取当前 cell 的rowSpan时,我们都依赖于 props 。通过render时,则必然需要调渲染 子节点 方法,而在调用渲染 子节点 方法就不能假设没有副作用,因而我们需要将这次的渲染如实反馈到页面上。而后者onCell则不会有这个问题。


因而在最新版本,我们把所有示例都替换成了onCell版本。同时提示用户render返回 props 下个大版本将被废弃。

最后


你会发现,解决某一个单一问题时往往很容易。我们可以针对自己的场景做到极致的优化。但是当业务场景的增加,你的组件库需要考虑的不单单是性能问题。一个更重要的点就是不能因为过渡优化而埋坑。在 Table 上,则是非常典型的取舍问题。因而这是为什么你看到了shouldCellUpdate而不是自动去 memo。我曾经和一些 Table 的开发者交流过上面的 edge case,但是往往讨论的最终结果就是这个解决了我的问题就行了,所以我不用 antd Table。人人都想要银弹,但是没有银弹。世间充满了不完美。


此外,除了上面几个点之外。Table 在维护过程中也有时候会引入一些真性能问题。比如 context 导致的 rerender、Resizable 导致的 rerender 等等等等。这些遇到就会修掉。本身就是 Table 的 BUG,没什么好说的。以上。

相关文章
|
2月前
|
存储 SQL 数据管理
TRUNCATE语句到底因何而慢?
综上所述,尽管 `TRUNCATE` 通常被视为快速的数据删除方法,但在处理特定的数据库配置、大型数据集、复杂的外键关系等方面,可能会意外地缓慢。因此,优化数据库性能和理解 `TRUNCATE` 在特定情况下的行为,对数据库管理员和开发人员来说是重要的。在进行此类操作之前,建议先测试并理解它们在您的特定环境中的表现,以便制定最有效的数据管理策略。
112 1
|
2月前
|
关系型数据库 数据库 PostgreSQL
pg下delete数据后。除了使用VACUUM FULL TABLE 才能释放磁盘空间外的方法。
【8月更文挑战第12天】pg下delete数据后。除了使用VACUUM FULL TABLE 才能释放磁盘空间外的方法。
92 1
|
5月前
|
SQL 缓存 算法
SQL 语句不要过多的 join
SQL 语句不要过多的 join
28 1
|
SQL 存储 自然语言处理
SQL语句命中索引,但还是执行很慢
MySQL的慢查询日志是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值(默认值10s)的SQL,则会被记录到慢查询日志中。
301 0
|
SQL 存储 Oracle
table()函数的使用,提高查询效率
table()函数的使用,提高查询效率
221 0
table()函数的使用,提高查询效率
|
SQL 关系型数据库 MySQL
MySQL排序ORDER BY与分页LIMIT,SQL,减少数据表的网络传输量,完整详细可收藏
MySQL排序ORDER BY与分页LIMIT,SQL,减少数据表的网络传输量,完整详细可收藏
128 0
|
SQL 数据库
慢SQL定位
慢SQL定位
84 0
|
SQL 测试技术 Go
SQL日志太大?教你一键清理
最近数据仓库时不时爆磁盘空间不足,导致定时任务执行失败,这可了得,要知道定时任务执行的可是每天的业务数据。 仔细检查,发现是日志文件爆满,这是咋回事呢? 原来数据仓库中,每天的定时任务需要从其他系统里面抽取数据过来,导致数据文件和日志文件的磁盘空间都增长的飞快。 数据不能清理,但是日志文件是可以清理的,说干就干。
SQL日志太大?教你一键清理
|
SQL 缓存 关系型数据库
RDS SQL Server - 专题分享 - 巧用执行计划缓存之Table Scan
# 背景引入 执行计划中的Table Scan或者是Clustered Index Scan会导致非常低下的查询性能,尤其是对于大表或者超大表。执行计划缓存是SQL Server内存管理中非常重要的特性,这篇系列文章我们探讨如何从执行计划缓存的角度来发现RDS SQL数据库引擎中的Table Scan行为,以及与之相应SQL查询语句详细信息。 # 问题分析 其实,我们大家都知道,Table
3142 0
|
SQL 关系型数据库
冗余数据JOIN导致的慢SQL优化一例
CASE 一个这样的查询,每个表都只有几千条数据,但是查询非常慢,几十秒不出结果。 select distinct abc.pro_col1, abc.col3 from t0 p INNER JOIN t1 abc on p.id=abc.par_col2
4838 0