From 5edcdc8200894321a8fe87fe39c7a72ba3cffa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Sun, 1 Jan 2023 11:19:33 +0800 Subject: [PATCH] docs: blog about re-render (#39941) * docs: rerender blog * docs: fix lint * Update docs/blog/render-times.zh-CN.md Co-authored-by: lijianan <574980606@qq.com> Co-authored-by: lijianan <574980606@qq.com> --- docs/blog/render-times.en-US.md | 169 ++++++++++++++++++++++++++++++++ docs/blog/render-times.zh-CN.md | 169 ++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 docs/blog/render-times.en-US.md create mode 100644 docs/blog/render-times.zh-CN.md diff --git a/docs/blog/render-times.en-US.md b/docs/blog/render-times.en-US.md new file mode 100644 index 0000000000..5593bcd5d7 --- /dev/null +++ b/docs/blog/render-times.en-US.md @@ -0,0 +1,169 @@ +--- +title: Unnecessary Rerender +date: 2022-12-31 +author: zombieJ +--- + +For heavy components, some bug fixes or new features can easily destroy the original performance optimization inadvertently over time. Recently, we are refactoring the Table to troubleshoot and restore the performance loss caused by some historical updates. Here, we introduce some common troubleshooting method and frequently meet problems. + +Before that, we recommend you to read the official [Perf tool](https://reactjs.org/docs/perf.html) to choose what you need. + +### Render count statistics + +In most cases, invalid rendering is not as dramatic as an un-optimized loop. However, in some scenarios such as large forms, tables, and lists, due to the large number of sub components, the performance impact of invalid rendering overlays is also terrible. + +For example, in antd v4, in order to improve Table hover highlighting experience of `rowSpan`, we added an event listener for `tr`, and added an additional `className` for the selected row in `td` to support multiple row highlighting capability. However, because `td` consumes `hoverStartRow` and `hoverEndRow` data in the context, non-related rows will [re-render](https://github.com/ant-design/ant-design/issues/33342) due to changes of `hoverStartRow` and `hoverEndRow`. + +Problems like this are repeated in heavy components, so we need some helper way to determine the number of renders. In the latest [`rc-table`](https://github.com/react-component/table), we encapsulate a [`useRenderTimes`](https://github.com/react-component/table/blob/ecf3fdb77523b370ee86e19164e95f00e65281a8/src/hooks/useRenderTimes.tsx) method. It will mark the monitored rendering times on React Dev Tools through React's `useDebugValue` in development mode: + +![VDM](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*vlwQQIcEXFkAAAAAAAAAAAAADrJ8AQ/original) + +```tsx +// Sample Code, please view real world code if needed +import React from 'react'; + +function useRenderTimes(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 + +Generally on the root node of the component, we will create a Context based on `props` and `state` to pass the aggregated data down. But in some cases, the actual content of the Context may not change and trigger the re-render of the child component: + +```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 ( + + + + ); +}; +``` + +In the example, although `prop1` and `prop2` have not changed, it is obvious that `value` in MyContext is a new object, causing the child component to re-render even if `prop1` has not changed. So we need to Memo the Context `value`: + +```tsx +// pseudocode +const context = React.useMemo(() => ({ prop1, prop2 }), [prop1, prop2]); + +return ( + + + +); +``` + +Note: You can configure eslint [rules](https://github.com/jsx-eslint/eslint-plugin-react/blob/3256c92ca1b3bc7ec3461a89c278c797e7dc18cb/docs/rules/jsx-no-constructed-context-values.md) to avoid this case. + +#### Split Context + +Also, refer to the example above. If we put both `prop1` and `prop2` in the Context, then even if `prop1` does not change, `prop2` changes will cause the child component to re-render. Therefore, we can split the Context into several according to the function, thereby reducing the scope of influence: + +```tsx +// pseudocode +const MyContext1 = React.createContext<{ prop1: string }>(); +const MyContext2 = React.createContext<{ prop2: string }>(); + +// Child +const { prop1 } = React.useContext(MyContext1); + +// Root + + + + +; +``` + +In `rc-table`, we split it into multiple to optimize rendering performance: + +- BodyContext +- ExpandedRowContext +- HoverContext +- PerfContext +- ResizeContext +- StickyContext +- TableContext + +#### useContextSelector + +If you have used Redux, then you may be familiar with `useSelector`, which only rerender when the data that needs to be consumed changes. In React, there is also a related RFC([#118](https://github.com/reactjs/rfcs/pull/118))([#119](https://github.com/reactjs/rfcs/pull/119)) about `useContextSelector`, which will also be implemented in React 18 in the future: + +![React 18](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*-UFKR7TTSv0AAAAAAAAAAAAADrJ8AQ/original) + +Before the API is officially launched, there are many third-party libraries implement (of course, you can also use redux directly). It is no longer necessary to consider the problem of function splitting Context through `useContextSelector`, which also reduces the mental burden of developers: + +```tsx +// pseudocode +const Child = React.memo(() => { + const prop1 = useContextSelector(MyContext, (context) => context.prop1); + return <>{prop1}; +}); +``` + +### Closure problem + +After optimizing in various ways, we still have to face a problem. If some rendering needs to pass through the external render method, and it happens that the method uses a closure. Then `React.memo` is unaware: + +```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 ; +}; +``` + +Due to the existence of closures, we cannot determine whether the final dom has changed before calling the `render` method, which is why we optimized the Table through memo in the early days of antd v4 and removed some of it over time (Actually, Table still has some scenarios where this problem needs to be solved). + +Considering that Table provides `shouldCellUpdate` method, we plan to adjust Table rendering logic in the future. When the Parent node renders, the Table will be completely re-rendered, and when the Table is updated internally (such as horizontal scrolling position synchronization), it will hit the cache and skip. + +### Finally + +antd Table optimization is still in progress, and we will continue to pay attention to new features of React and new ideas from the community. If you have any ideas, welcome to discuss on Github. In addition, for the suggestion of self-developed components, we recommend that after each optimization, a corresponding test case should be created, and the source issue should be noted for future retrospection. That's all. Thank you for reading. diff --git a/docs/blog/render-times.zh-CN.md b/docs/blog/render-times.zh-CN.md new file mode 100644 index 0000000000..9c8503da7a --- /dev/null +++ b/docs/blog/render-times.zh-CN.md @@ -0,0 +1,169 @@ +--- +title: 非必要的渲染 +date: 2022-12-31 +author: zombieJ +--- + +对于重型组件而言,随着时间推移,一些 BUG Fix 或者新增 Feature 很容易不经意间将原本的性能优化给破坏掉。而最近,我们在对 Table 进行重构将一些历史更新导致的性能损失进行排查并恢复。在此,我们介绍一些常用的排查技巧以及常见问题。 + +在此之前,我们建议你先阅读官方的 [性能工具](https://reactjs.org/docs/perf.html) 以选择你需要调试的内容。 + +### 渲染次数统计 + +在大部分情况下,无效的渲染相对于未优化的循环而言,体感并没有那么强烈。但是在某一些场景诸如大型表单、表格、列表下,由于其子组件众多,无效的渲染叠加后其性能影响也十分可怕。 + +举个例子,在 antd v4 中,我们为了提升 `rowSpan` Table Hover 的高亮体验,我们为 `tr` 添加了事件监听,同时在 `td` 中为选中行添加额外的 `className` 以支持多行高亮能力。但是由于 `td` 消费了 context 中 `hoverStartRow` 和 `hoverEndRow` 数据,导致了非相关 Row 都会因为 `hoverStartRow` 和 `hoverEndRow` 变化而[重新渲染](https://github.com/ant-design/ant-design/issues/33342)。 + +诸如此类的问题在重型组件循环往复,因而我们需要一些辅助方式来确定渲染次数。在最新的 [`rc-table`](https://github.com/react-component/table) 中,我们封装了一个 [`useRenderTimes`](https://github.com/react-component/table/blob/ecf3fdb77523b370ee86e19164e95f00e65281a8/src/hooks/useRenderTimes.tsx) 方法。它会在开发模式下通过 React 的 `useDebugValue` 将监听的渲染次数标注在 React Dev Tools 上: + +![VDM](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*vlwQQIcEXFkAAAAAAAAAAAAADrJ8AQ/original) + +```tsx +// Sample Code, please view real world code if needed +import React from 'react'; + +function useRenderTimes(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 ( + + + + ); +}; +``` + +在示例中,虽然 `prop1` 和 `prop2` 并没有变化,但是显然 MyContext 里的 `value` 是一个新的 Object 导致子组件即便 `prop1` 没有变化也会重新渲染。因而我们需要对 Context `value` 进行 Memo: + +```tsx +// pseudocode +const context = React.useMemo(() => ({ prop1, prop2 }), [prop1, prop2]); + +return ( + + + +); +``` + +注:你可以配置 eslint [规则](https://github.com/jsx-eslint/eslint-plugin-react/blob/3256c92ca1b3bc7ec3461a89c278c797e7dc18cb/docs/rules/jsx-no-constructed-context-values.md) 来避免遗漏。 + +#### 拆分 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 + + + + +; +``` + +在 `rc-table` 中,我们将其拆分为多个以优化渲染性能: + +- BodyContext +- ExpandedRowContext +- HoverContext +- PerfContext +- ResizeContext +- StickyContext +- TableContext + +#### useContextSelector + +如果你使用过 Redux,那么你可能会对 `useSelector` 比较熟悉,它只会在需要消费的数据变更时才会触发更新。在 React 中,也同样有相关的 RFC([#118](https://github.com/reactjs/rfcs/pull/118))([#119](https://github.com/reactjs/rfcs/pull/119)),未来在 React 18 也将实装: + +![React 18](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*-UFKR7TTSv0AAAAAAAAAAAAADrJ8AQ/original) + +在 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 ; +}; +``` + +由于闭包的存在,在调用 `render` 方法之前我们无法确定组件最终形态是否发生变化,这也是为何在 antd v4 早期我们通过 memo 对 Table 进行了优化而随着时间推移又将一部分移除的原因(实际上,Table 仍然有一些场景会遇到这个问题需要解决)。 + +考虑到 Table 提供了 `shouldCellUpdate` 方法,我们准备未来调整 Table 渲染逻辑。当 Parent 节点渲染时,Table 会完整的重新渲染,而当 Table 内部更新时(例如水平滚动位置同步),则会命中缓存而跳过。 + +### 最后 + +antd 的 Table 优化仍在进行中,我们也会持续关注 React 的新特性,以及社区的新思路。如果你有任何想法,欢迎在 Github 留言讨论。此外,对于自行研发组件的建议,我们推荐在每次完成优化后,都要创建对应的测试用例,并且备注来源 issue 以便于未来的回溯。以上。