Skip to content

Commit 2bb803e

Browse files
authored
perf: Cell not render if no additional perf cost (#752)
* init * perf: Not re-render when cell props in normal one * test: Test coverage
1 parent e23e391 commit 2bb803e

File tree

4 files changed

+146
-23
lines changed

4 files changed

+146
-23
lines changed

src/Body/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ResizeContext from '../context/ResizeContext';
88
import BodyRow from './BodyRow';
99
import useFlattenRecords from '../hooks/useFlattenRecords';
1010
import HoverContext from '../context/HoverContext';
11+
import PerfContext, { PerfRecord } from '../context/PerfContext';
1112
import MeasureRow from './MeasureRow';
1213

1314
export interface BodyProps<RecordType> {
@@ -38,6 +39,11 @@ function Body<RecordType>({
3839
const flattenData: { record: RecordType; indent: number; index: number }[] =
3940
useFlattenRecords<RecordType>(data, childrenColumnName, expandedKeys, getRowKey);
4041

42+
// =================== Performance ====================
43+
const perfRef = React.useRef<PerfRecord>({
44+
renderWithProps: false,
45+
});
46+
4147
// ====================== Hover =======================
4248
const [startRow, setStartRow] = React.useState(-1);
4349
const [endRow, setEndRow] = React.useState(-1);
@@ -132,7 +138,11 @@ function Body<RecordType>({
132138
flattenData,
133139
]);
134140

135-
return <HoverContext.Provider value={hoverContext}>{bodyNode}</HoverContext.Provider>;
141+
return (
142+
<PerfContext.Provider value={perfRef.current}>
143+
<HoverContext.Provider value={hoverContext}>{bodyNode}</HoverContext.Provider>
144+
</PerfContext.Provider>
145+
);
136146
}
137147

138148
const MemoBody = React.memo(Body);

src/Cell/index.tsx

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import StickyContext from '../context/StickyContext';
1717
import HoverContext from '../context/HoverContext';
1818
import type { HoverContextProps } from '../context/HoverContext';
1919
import warning from 'rc-util/lib/warning';
20+
import PerfContext from '../context/PerfContext';
2021

2122
/** Check if cell is in hover range */
2223
function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) {
@@ -120,19 +121,23 @@ function Cell<RecordType extends DefaultRecordType>(
120121
): React.ReactElement {
121122
const cellPrefixCls = `${prefixCls}-cell`;
122123

124+
const perfRecord = React.useContext(PerfContext);
123125
const supportSticky = React.useContext(StickyContext);
124126

125127
// ==================== Child Node ====================
126-
let cellProps: CellType<RecordType>;
127-
let childNode: React.ReactNode;
128+
const [childNode, legacyCellProps] = React.useMemo<
129+
[React.ReactNode, CellType<RecordType>] | [React.ReactNode]
130+
>(() => {
131+
if (validateValue(children)) {
132+
return [children];
133+
}
128134

129-
if (validateValue(children)) {
130-
childNode = children;
131-
} else {
132135
const value = getPathValue<object | React.ReactNode, RecordType>(record, dataIndex);
133136

134137
// Customize render node
135-
childNode = value;
138+
let returnChildNode = value;
139+
let returnCellProps: CellType<RecordType> | undefined = undefined;
140+
136141
if (render) {
137142
const renderData = render(value, record, renderIndex);
138143

@@ -143,25 +148,41 @@ function Cell<RecordType extends DefaultRecordType>(
143148
'`columns.render` return cell props is deprecated with perf issue, please use `onCell` instead.',
144149
);
145150
}
146-
childNode = renderData.children;
147-
cellProps = renderData.props;
151+
returnChildNode = renderData.children;
152+
returnCellProps = renderData.props;
153+
perfRecord.renderWithProps = true;
148154
} else {
149-
childNode = renderData;
155+
returnChildNode = renderData;
150156
}
151157
}
152-
}
158+
159+
return [returnChildNode, returnCellProps];
160+
}, [
161+
/* eslint-disable react-hooks/exhaustive-deps */
162+
// Always re-render if `renderWithProps`
163+
perfRecord.renderWithProps ? Math.random() : 0,
164+
/* eslint-enable */
165+
children,
166+
dataIndex,
167+
perfRecord,
168+
record,
169+
render,
170+
renderIndex,
171+
]);
172+
173+
let mergedChildNode = childNode;
153174

154175
// Not crash if final `childNode` is not validate ReactNode
155176
if (
156-
typeof childNode === 'object' &&
157-
!Array.isArray(childNode) &&
158-
!React.isValidElement(childNode)
177+
typeof mergedChildNode === 'object' &&
178+
!Array.isArray(mergedChildNode) &&
179+
!React.isValidElement(mergedChildNode)
159180
) {
160-
childNode = null;
181+
mergedChildNode = null;
161182
}
162183

163184
if (ellipsis && (lastFixLeft || firstFixRight)) {
164-
childNode = <span className={`${cellPrefixCls}-content`}>{childNode}</span>;
185+
mergedChildNode = <span className={`${cellPrefixCls}-content`}>{mergedChildNode}</span>;
165186
}
166187

167188
const {
@@ -170,7 +191,7 @@ function Cell<RecordType extends DefaultRecordType>(
170191
style: cellStyle,
171192
className: cellClassName,
172193
...restCellProps
173-
} = cellProps || {};
194+
} = legacyCellProps || {};
174195
const mergedColSpan = (cellColSpan !== undefined ? cellColSpan : colSpan) ?? 1;
175196
const mergedRowSpan = (cellRowSpan !== undefined ? cellRowSpan : rowSpan) ?? 1;
176197

@@ -220,10 +241,13 @@ function Cell<RecordType extends DefaultRecordType>(
220241
let title: string;
221242
const ellipsisConfig: CellEllipsisType = ellipsis === true ? { showTitle: true } : ellipsis;
222243
if (ellipsisConfig && (ellipsisConfig.showTitle || rowType === 'header')) {
223-
if (typeof childNode === 'string' || typeof childNode === 'number') {
224-
title = childNode.toString();
225-
} else if (React.isValidElement(childNode) && typeof childNode.props.children === 'string') {
226-
title = childNode.props.children;
244+
if (typeof mergedChildNode === 'string' || typeof mergedChildNode === 'number') {
245+
title = mergedChildNode.toString();
246+
} else if (
247+
React.isValidElement(mergedChildNode) &&
248+
typeof mergedChildNode.props.children === 'string'
249+
) {
250+
title = mergedChildNode.props.children;
227251
}
228252
}
229253

@@ -248,7 +272,7 @@ function Cell<RecordType extends DefaultRecordType>(
248272
[`${cellPrefixCls}-ellipsis`]: ellipsis,
249273
[`${cellPrefixCls}-with-append`]: appendNode,
250274
[`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky && supportSticky,
251-
[`${cellPrefixCls}-row-hover`]: !cellProps && hovering,
275+
[`${cellPrefixCls}-row-hover`]: !legacyCellProps && hovering,
252276
},
253277
additionalProps.className,
254278
cellClassName,
@@ -262,7 +286,7 @@ function Cell<RecordType extends DefaultRecordType>(
262286
return (
263287
<Component {...componentProps}>
264288
{appendNode}
265-
{childNode}
289+
{mergedChildNode}
266290
</Component>
267291
);
268292
}

src/context/PerfContext.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
export interface PerfRecord {
4+
renderWithProps: boolean;
5+
}
6+
7+
const PerfContext = React.createContext<PerfRecord>({
8+
renderWithProps: false,
9+
});
10+
11+
export default PerfContext;

tests/Hover.spec.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,82 @@ describe('Table.Hover', () => {
124124
wrapper.find('tbody td').at(1).simulate('mouseLeave');
125125
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
126126
});
127+
128+
describe('perf', () => {
129+
it('legacy mode should render every time', () => {
130+
let renderTimes = 0;
131+
132+
const wrapper = mount(
133+
createTable({
134+
columns: [
135+
{
136+
render: () => {
137+
renderTimes += 1;
138+
return {
139+
children: null,
140+
};
141+
},
142+
},
143+
],
144+
}),
145+
);
146+
147+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
148+
149+
// Hover 0-0
150+
renderTimes = 0;
151+
wrapper.find('tbody td').at(0).simulate('mouseEnter');
152+
expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1);
153+
expect(renderTimes).toBe(1);
154+
155+
// Hover 0-1
156+
renderTimes = 0;
157+
wrapper.find('tbody td').at(1).simulate('mouseEnter');
158+
expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1);
159+
expect(renderTimes).toBe(2);
160+
161+
// Mouse leave
162+
renderTimes = 0;
163+
wrapper.find('tbody td').at(1).simulate('mouseLeave');
164+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
165+
expect(renderTimes).toBe(1);
166+
});
167+
168+
it('perf mode to save render times', () => {
169+
let renderTimes = 0;
170+
171+
const wrapper = mount(
172+
createTable({
173+
columns: [
174+
{
175+
render: () => {
176+
renderTimes += 1;
177+
return null;
178+
},
179+
},
180+
],
181+
}),
182+
);
183+
184+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
185+
186+
// Hover 0-0
187+
renderTimes = 0;
188+
wrapper.find('tbody td').at(0).simulate('mouseEnter');
189+
expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1);
190+
expect(renderTimes).toBe(0);
191+
192+
// Hover 0-1
193+
renderTimes = 0;
194+
wrapper.find('tbody td').at(1).simulate('mouseEnter');
195+
expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1);
196+
expect(renderTimes).toBe(0);
197+
198+
// Mouse leave
199+
renderTimes = 0;
200+
wrapper.find('tbody td').at(1).simulate('mouseLeave');
201+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
202+
expect(renderTimes).toBe(0);
203+
});
204+
});
127205
});

0 commit comments

Comments
 (0)