logoAnt Design

⌘ K
  • 设计
  • 研发
  • 组件
  • 博客
  • 资源
  • 国内镜像
5.25.4
  • 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
    Twitter
    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-互动图形解决方案
    xtech logo蚂蚁体验科技
    主题编辑器
    Made with ❤ by
    蚂蚁集团和 Ant Design 开源社区

    对于重型组件而言,随着时间推移,一些 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 以便于未来的回溯。以上。