logoAnt Design

⌘ K
  • 设计
  • 研发
  • 组件
  • 博客
  • 资源
  • 国内镜像
5.27.5
  • v6 的一些 CSS 琐事
  • 👀 视觉回归测试
  • 为什么禁用日期这么难?
  • 封装 Form.Item 实现数组转对象
  • 行省略计算
  • 📢 v4 维护周期截止
  • antd 里常用的 TypeScript 工具方法
  • 一个构建的幽灵
  • 当 Ant Design 遇上 CSS 变量
  • API 的历史债务
  • 灵动的 Notification
  • 色彩模型与颜色选择器
  • 主题拓展
  • 虚拟表格来了!
  • 快乐工作主题(一)
  • 动态样式去哪儿了?
  • Suspense 引发的样式丢失问题
  • 打包体积优化
  • 你好,GitHub Actions
  • 所见即所得
  • 静态方法之痛
  • SSR 静态样式导出
  • 依赖排查
  • 贡献者开发维护指南
  • 转载-如何提交无法解答的问题
  • 新的 Tooltip 对齐方式
  • 非必要的渲染
  • 如何成长为 Collaborator
  • Modal hook 的有趣 BUG
  • antd 测试库迁移的那些事儿
  • Tree 的勾选传导
  • getContainer 的一些变化
  • 组件级别的 CSS-in-JS

非必要的渲染

2022-12-31
@zombieJ

文章被以下专栏收录:

antd

Ant Design

一个 UI 设计体系
我有想法,去参与讨论
antd

Ant Design

Ant Design 官方专栏
我有想法,去参与讨论
antd

Ant Design

Juejin logoAnt Design 开源专栏
Juejin logo我有想法,去参与讨论
文档贡献者
新的 Tooltip 对齐方式如何成长为 Collaborator

相关资源

Ant Design X
Ant Design Charts
Ant Design Pro
Pro Components
Ant Design Mobile
Ant Design Mini
Ant Design Web3
Ant Design Landing-首页模板集
Scaffolds-脚手架市场
Umi-React 应用开发框架
dumi-组件/文档研发工具
qiankun-微前端框架
Ant Motion-设计动效
国内镜像站点 🇨🇳

社区

Awesome Ant Design
Medium
X
yuque logoAnt Design 语雀专栏
Ant Design 知乎专栏
体验科技专栏
seeconf logoSEE Conf-蚂蚁体验科技大会
加入我们

帮助

GitHub
更新日志
常见问题
报告 Bug
议题
讨论区
StackOverflow
SegmentFault

Ant XTech logo更多产品

yuque logo语雀-构建你的数字花园
AntV logoAntV-数据可视化解决方案
Egg logoEgg-企业级 Node.js 框架
Kitchen logoKitchen-Sketch 工具集
Galacean logoGalacean-互动图形解决方案
WeaveFox logoWeaveFox-前端智能研发
xtech logo蚂蚁体验科技
主题编辑器
Made with ❤ by
蚂蚁集团和 Ant Design 开源社区
loading

对于重型组件而言,随着时间推移,一些 BUG Fix 或者新增 Feature 很容易不经意间将原本的性能优化给破坏掉。而最近,我们在对 Table 进行重构将一些历史更新导致的性能损失进行排查并恢复。在此,我们介绍一些常用的排查技巧以及常见问题。

在此之前,我们建议你先阅读官方的 性能工具 以选择你需要调试的内容。

渲染次数统计

在大部分情况下,无效的渲染相对于未优化的循环而言,体感并没有那么强烈。但是在某一些场景诸如大型表单、表格、列表下,由于其子组件众多,无效的渲染叠加后其性能影响也十分可怕。

举个例子,在 antd v4 中,我们为了提升 rowSpan Table Hover 的高亮体验,我们为 tr 添加了事件监听,同时在 td 中为选中行添加额外的 className 以支持多行高亮能力。但是由于 td 消费了 context 中 hoverStartRow 和 hoverEndRow 数据,导致了非相关 Row 都会因为 hoverStartRow 和 hoverEndRow 变化而重新渲染。

诸如此类的问题在重型组件循环往复,因而我们需要一些辅助方式来确定渲染次数。在最新的 rc-table 中,我们封装了一个 useRenderTimes 方法。它会在开发模式下通过 React 的 useDebugValue 将监听的渲染次数标注在 React Dev Tools 上:

VDM

tsx
// Sample Code, please view real world code if needed
import React from 'react';
function useRenderTimes<T>(props: T) {
// Render times
const timesRef = React.useRef(0);
timesRef.current += 1;
// Cache for prev props
const cacheProps = React.useRef(props);
const changedPropKeys = getDiff(props, cacheProps.current); // Some compare logic
React.useDebugValue(timesRef.current);
React.useDebugValue(changedPropKeys);
cacheProps.current = props;
}
export default process.env.NODE_ENV !== 'production' ? useRenderTimes : () => {};

Context

useMemo

一般在组件的根节点上,我们会根据 props 和 state 创建一个 Context 来将聚合数据传递下去。但是在某些情况下可能 Context 实际内容没有变化也触发子组件的重新渲染:

tsx
// pseudocode
const MyContext = React.createContext<{ prop1: string; prop2: string }>();
const Child = React.memo(() => {
const { prop1 } = React.useContext(MyContext);
return <>{prop1}</>;
});
const Root = ({ prop1, prop2 }) => {
const [count, setCount] = React.useState(0);
// Some logic to trigger rerender
React.useEffect(() => {
setCount(1);
}, []);
return (
<MyContext.Provider value={{ prop1, prop2 }}>
<Child />
</MyContext.Provider>
);
};

在示例中,虽然 prop1 和 prop2 并没有变化,但是显然 MyContext 里的 value 是一个新的 Object 导致子组件即便 prop1 没有变化也会重新渲染。因而我们需要对 Context value 进行 Memo:

tsx
// pseudocode
const context = React.useMemo(() => ({ prop1, prop2 }), [prop1, prop2]);
return (
<MyContext.Provider value={context}>
<Child />
</MyContext.Provider>
);

注:你可以配置 eslint 规则 来避免遗漏。

拆分 Context

此外,参考上面的示例。如果我们将 prop1 和 prop2 都放在 Context 中,那么即便 prop1 没有变化,prop2 变化了,也会导致子组件重新渲染。因而我们可以根据功能将 Context 拆分成多个,从而减小影响范围:

tsx
// pseudocode
const MyContext1 = React.createContext<{ prop1: string }>();
const MyContext2 = React.createContext<{ prop2: string }>();
// Child
const { prop1 } = React.useContext(MyContext1);
// Root
<MyContext1.Provider value={context1}>
<MyContext2.Provider value={context2}>
<Child />
</MyContext2.Provider>
</MyContext1.Provider>;

在 rc-table 中,我们将其拆分为多个以优化渲染性能:

  • BodyContext
  • ExpandedRowContext
  • HoverContext
  • PerfContext
  • ResizeContext
  • StickyContext
  • TableContext

useContextSelector

如果你使用过 Redux,那么你可能会对 useSelector 比较熟悉,它只会在需要消费的数据变更时才会触发更新。在 React 中,也同样有相关的 RFC(#118)(#119),未来在 React 18 也将实装:

React 18

在 API 正式落地之前,业界也有不少三方库实现该 API(当然,你也可以直接使用 redux)。通过 useContextSelector 就不再需要考虑功能拆分 Context 的问题,这也降低了开发者的心智负担:

tsx
// pseudocode
const Child = React.memo(() => {
const prop1 = useContextSelector(MyContext, (context) => context.prop1);
return <>{prop1}</>;
});

闭包问题

在通过各种方式优化过后,我们还不得不面对一个问题。如果某些渲染需要通过外界的 render 方式,并且碰巧该方式使用了闭包。那么 React.memo 是无法感知的:

tsx
// pseudocode
import React from 'react';
const MyComponent = React.memo(({ valueRender }: { valueRender: () => React.ReactElement }) =>
valueRender(),
);
const App = () => {
const countRef = React.useRef(0);
const [, forceUpdate] = React.useState({});
React.useEffect(() => {
countRef.current += 1;
forceUpdate({});
}, []);
// In real world, class component often meet this by `this.state`
const valueRender = React.useCallback(() => countRef.current, []);
return <MyComponent valueRender={valueRender} />;
};

由于闭包的存在,在调用 render 方法之前我们无法确定组件最终形态是否发生变化,这也是为何在 antd v4 早期我们通过 memo 对 Table 进行了优化而随着时间推移又将一部分移除的原因(实际上,Table 仍然有一些场景会遇到这个问题需要解决)。

考虑到 Table 提供了 shouldCellUpdate 方法,我们准备未来调整 Table 渲染逻辑。当 Parent 节点渲染时,Table 会完整的重新渲染,而当 Table 内部更新时(例如水平滚动位置同步),则会命中缓存而跳过。

最后

antd 的 Table 优化仍在进行中,我们也会持续关注 React 的新特性,以及社区的新思路。如果你有任何想法,欢迎在 GitHub 留言讨论。此外,对于自行研发组件的建议,我们推荐在每次完成优化后,都要创建对应的测试用例,并且备注来源 issue 以便于未来的回溯。以上。