From b895f4ce6d700df8fed523be37d581f65ba076f0 Mon Sep 17 00:00:00 2001 From: Curly Brackets Date: Tue, 30 Nov 2021 10:59:58 +0800 Subject: [PATCH 1/2] perf: observe columns with one ResizeObserver instance --- package.json | 4 ++- src/Body/MeasureCell.tsx | 8 ++--- src/Body/MeasureRow.tsx | 59 ++++++++++++++++++++++++++++++++++++ src/Body/index.tsx | 20 ++++-------- tests/FixedColumn-IE.spec.js | 14 +++++++-- tests/FixedColumn.spec.js | 12 ++++++-- tests/FixedHeader.spec.js | 52 +++++++++++++++++++++++++------ 7 files changed, 134 insertions(+), 35 deletions(-) create mode 100644 src/Body/MeasureRow.tsx diff --git a/package.json b/package.json index 1068603c4..e17fd2a18 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,15 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "rc-resize-observer": "^1.0.0", + "lodash": "^4.17.21", + "rc-resize-observer": "^1.1.0", "rc-util": "^5.14.0", "shallowequal": "^1.1.0" }, "devDependencies": { "@types/enzyme": "^3.10.5", "@types/jest": "^26.0.3", + "@types/lodash": "^4.14.177", "@types/react": "^17.0.35", "@types/react-dom": "^17.0.10", "@umijs/fabric": "^2.0.0", diff --git a/src/Body/MeasureCell.tsx b/src/Body/MeasureCell.tsx index b20de4062..1fc8d9cdf 100644 --- a/src/Body/MeasureCell.tsx +++ b/src/Body/MeasureCell.tsx @@ -7,7 +7,7 @@ export interface MeasureCellProps { } export default function MeasureCell({ columnKey, onColumnResize }: MeasureCellProps) { - const cellRef = React.useRef(); + const cellRef = React.useRef(); React.useEffect(() => { if (cellRef.current) { @@ -16,11 +16,7 @@ export default function MeasureCell({ columnKey, onColumnResize }: MeasureCellPr }, []); return ( - { - onColumnResize(columnKey, offsetWidth); - }} - > +
 
diff --git a/src/Body/MeasureRow.tsx b/src/Body/MeasureRow.tsx new file mode 100644 index 000000000..990800997 --- /dev/null +++ b/src/Body/MeasureRow.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import ResizeObserver from 'rc-resize-observer'; +import { debounce } from 'lodash'; +import MeasureCell from './MeasureCell'; + +export interface MeasureCellProps { + prefixCls: string; + onColumnResize: (key: React.Key, width: number) => void; + columnsKey: React.Key[]; +} + +export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: MeasureCellProps) { + // debounce the continuous resize, e.g. window resize + const resizedColumnsRef = React.useRef(new Map()); + const onColumnResizeRef = React.useRef(onColumnResize); + onColumnResizeRef.current = onColumnResize; + const debounceColumnResize = React.useMemo( + () => + debounce( + () => { + resizedColumnsRef.current.forEach((width, columnKey) => { + onColumnResizeRef.current(columnKey, width); + }); + resizedColumnsRef.current.clear(); + }, + 1200, + { + leading: true, + trailing: true, + }, + ), + [], + ); + React.useEffect(() => { + return () => { + debounceColumnResize.cancel(); + }; + }, []); + return ( + + { + infoList.forEach(({ data: columnKey, size }) => { + resizedColumnsRef.current.set(columnKey, size.offsetWidth); + }); + debounceColumnResize(); + }} + > + {columnsKey.map(columnKey => ( + + ))} + + + ); +} diff --git a/src/Body/index.tsx b/src/Body/index.tsx index b8049690e..8d070f1d3 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -5,10 +5,10 @@ import ExpandedRow from './ExpandedRow'; import BodyContext from '../context/BodyContext'; import { getColumnsKey } from '../utils/valueUtil'; import ResizeContext from '../context/ResizeContext'; -import MeasureCell from './MeasureCell'; import BodyRow from './BodyRow'; import useFlattenRecords from '../hooks/useFlattenRecords'; import HoverContext from '../context/HoverContext'; +import MeasureRow from './MeasureRow'; export interface BodyProps { data: readonly RecordType[]; @@ -102,19 +102,11 @@ function Body({ {/* Measure body column width with additional hidden col */} {measureColumnWidth && ( - - {columnsKey.map(columnKey => ( - - ))} - + )} {rows} diff --git a/tests/FixedColumn-IE.spec.js b/tests/FixedColumn-IE.spec.js index 90b9bb653..0c1ecafc2 100644 --- a/tests/FixedColumn-IE.spec.js +++ b/tests/FixedColumn-IE.spec.js @@ -5,6 +5,7 @@ import { spyElementPrototype } from 'rc-util/lib/test/domHook'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { isStyleSupport } from 'rc-util/lib/Dom/styleChecker'; import Table from '../src'; +import RcResizeObserver from 'rc-resize-observer'; jest.mock('rc-util/lib/Dom/styleChecker', () => { return { @@ -46,11 +47,20 @@ describe('Table.FixedColumn', () => { const wrapper = mount(); act(() => { - wrapper.find('table ResizeObserver').first().props().onResize({ width: 93, offsetWidth: 93 }); + wrapper + .find(RcResizeObserver.Collection) + .first() + .props() + .onBatchResize([ + { + data: wrapper.find('table ResizeObserver').first().props().data, + size: { width: 93, offsetWidth: 93 }, + }, + ]); }); await act(async () => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); await Promise.resolve(); wrapper.update(); }); diff --git a/tests/FixedColumn.spec.js b/tests/FixedColumn.spec.js index e0bff211e..a11119cb4 100644 --- a/tests/FixedColumn.spec.js +++ b/tests/FixedColumn.spec.js @@ -4,6 +4,7 @@ import { act } from 'react-dom/test-utils'; import { resetWarned } from 'rc-util/lib/warning'; import { spyElementPrototype } from 'rc-util/lib/test/domHook'; import Table from '../src'; +import RcResizeObserver from 'rc-resize-observer'; describe('Table.FixedColumn', () => { let domSpy; @@ -59,13 +60,18 @@ describe('Table.FixedColumn', () => { act(() => { wrapper - .find('table ResizeObserver') + .find(RcResizeObserver.Collection) .first() .props() - .onResize({ width: 93, offsetWidth: 93 }); + .onBatchResize([ + { + data: wrapper.find('table ResizeObserver').first().props().data, + size: { width: 93, offsetWidth: 93 }, + }, + ]); }); await act(async () => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); await Promise.resolve(); wrapper.update(); }); diff --git a/tests/FixedHeader.spec.js b/tests/FixedHeader.spec.js index 5fa158837..c3f72e8b9 100644 --- a/tests/FixedHeader.spec.js +++ b/tests/FixedHeader.spec.js @@ -3,6 +3,7 @@ import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { spyElementPrototype } from 'rc-util/lib/test/domHook'; import Table, { INTERNAL_COL_DEFINE } from '../src'; +import RcResizeObserver from 'rc-resize-observer'; describe('Table.FixedHeader', () => { let domSpy; @@ -23,7 +24,6 @@ describe('Table.FixedHeader', () => { }); it('should work', async () => { - jest.useFakeTimers(); const col1 = { dataIndex: 'light', width: 100 }; const col2 = { dataIndex: 'bamboo', width: 200 }; const col3 = { dataIndex: 'empty', width: 0 }; @@ -35,12 +35,27 @@ describe('Table.FixedHeader', () => { />, ); - wrapper.find('ResizeObserver').at(0).props().onResize({ width: 100, offsetWidth: 100 }); - wrapper.find('ResizeObserver').at(1).props().onResize({ width: 200, offsetWidth: 200 }); - wrapper.find('ResizeObserver').at(2).props().onResize({ width: 0, offsetWidth: 0 }); + wrapper + .find(RcResizeObserver.Collection) + .first() + .props() + .onBatchResize([ + { + data: wrapper.find('ResizeObserver').at(0).props().data, + size: { width: 100, offsetWidth: 100 }, + }, + { + data: wrapper.find('ResizeObserver').at(1).props().data, + size: { width: 200, offsetWidth: 200 }, + }, + { + data: wrapper.find('ResizeObserver').at(2).props().data, + size: { width: 0, offsetWidth: 0 }, + }, + ]); await act(async () => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); await Promise.resolve(); wrapper.update(); }); @@ -147,9 +162,18 @@ describe('Table.FixedHeader', () => { />, ); - wrapper.find('ResizeObserver').at(0).props().onResize({ width: 93, offsetWidth: 93 }); + wrapper + .find(RcResizeObserver.Collection) + .first() + .props() + .onBatchResize([ + { + data: wrapper.find('ResizeObserver').at(0).props().data, + size: { width: 93, offsetWidth: 93 }, + }, + ]); await act(async () => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); await Promise.resolve(); wrapper.update(); }); @@ -161,9 +185,19 @@ describe('Table.FixedHeader', () => { // Hide Table should not modify column width visible = false; - wrapper.find('ResizeObserver').at(0).props().onResize({ width: 0, offsetWidth: 0 }); + wrapper + .find(RcResizeObserver.Collection) + .first() + .props() + .onBatchResize([ + { + data: wrapper.find('ResizeObserver').at(0).props().data, + size: { width: 0, offsetWidth: 0 }, + }, + ]); + act(() => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); wrapper.update(); }); From 018c4a8c0228f95fd09e618a85dd2a7e010115c9 Mon Sep 17 00:00:00 2001 From: Curly Brackets Date: Tue, 30 Nov 2021 13:23:43 +0800 Subject: [PATCH 2/2] refactor: debounce replace with raf delay 2 frames --- package.json | 2 -- src/Body/MeasureRow.tsx | 41 ++++++++++++++++-------------------- tests/FixedColumn-IE.spec.js | 2 +- tests/FixedColumn.spec.js | 2 +- tests/FixedHeader.spec.js | 7 +++--- 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e17fd2a18..7cc645baa 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "lodash": "^4.17.21", "rc-resize-observer": "^1.1.0", "rc-util": "^5.14.0", "shallowequal": "^1.1.0" @@ -63,7 +62,6 @@ "devDependencies": { "@types/enzyme": "^3.10.5", "@types/jest": "^26.0.3", - "@types/lodash": "^4.14.177", "@types/react": "^17.0.35", "@types/react-dom": "^17.0.10", "@umijs/fabric": "^2.0.0", diff --git a/src/Body/MeasureRow.tsx b/src/Body/MeasureRow.tsx index 990800997..be468f2ec 100644 --- a/src/Body/MeasureRow.tsx +++ b/src/Body/MeasureRow.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ResizeObserver from 'rc-resize-observer'; -import { debounce } from 'lodash'; import MeasureCell from './MeasureCell'; +import raf from 'rc-util/lib/raf'; export interface MeasureCellProps { prefixCls: string; @@ -10,30 +10,25 @@ export interface MeasureCellProps { } export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: MeasureCellProps) { - // debounce the continuous resize, e.g. window resize + // delay state update while resize continuously, e.g. window resize const resizedColumnsRef = React.useRef(new Map()); - const onColumnResizeRef = React.useRef(onColumnResize); - onColumnResizeRef.current = onColumnResize; - const debounceColumnResize = React.useMemo( - () => - debounce( - () => { - resizedColumnsRef.current.forEach((width, columnKey) => { - onColumnResizeRef.current(columnKey, width); - }); - resizedColumnsRef.current.clear(); - }, - 1200, - { - leading: true, - trailing: true, - }, - ), - [], - ); + const rafIdRef = React.useRef(null); + + const delayOnColumnResize = () => { + if (rafIdRef.current === null) { + rafIdRef.current = raf(() => { + resizedColumnsRef.current.forEach((width, columnKey) => { + onColumnResize(columnKey, width); + }); + resizedColumnsRef.current.clear(); + rafIdRef.current = null; + }, 2); + } + }; + React.useEffect(() => { return () => { - debounceColumnResize.cancel(); + raf.cancel(rafIdRef.current); }; }, []); return ( @@ -47,7 +42,7 @@ export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: Me infoList.forEach(({ data: columnKey, size }) => { resizedColumnsRef.current.set(columnKey, size.offsetWidth); }); - debounceColumnResize(); + delayOnColumnResize(); }} > {columnsKey.map(columnKey => ( diff --git a/tests/FixedColumn-IE.spec.js b/tests/FixedColumn-IE.spec.js index 0c1ecafc2..8d74411ff 100644 --- a/tests/FixedColumn-IE.spec.js +++ b/tests/FixedColumn-IE.spec.js @@ -60,7 +60,7 @@ describe('Table.FixedColumn', () => { }); await act(async () => { - jest.runOnlyPendingTimers(); + jest.runAllTimers(); await Promise.resolve(); wrapper.update(); }); diff --git a/tests/FixedColumn.spec.js b/tests/FixedColumn.spec.js index a11119cb4..eb53648bb 100644 --- a/tests/FixedColumn.spec.js +++ b/tests/FixedColumn.spec.js @@ -71,7 +71,7 @@ describe('Table.FixedColumn', () => { ]); }); await act(async () => { - jest.runOnlyPendingTimers(); + jest.runAllTimers(); await Promise.resolve(); wrapper.update(); }); diff --git a/tests/FixedHeader.spec.js b/tests/FixedHeader.spec.js index c3f72e8b9..00e1919b6 100644 --- a/tests/FixedHeader.spec.js +++ b/tests/FixedHeader.spec.js @@ -24,6 +24,7 @@ describe('Table.FixedHeader', () => { }); it('should work', async () => { + jest.useFakeTimers(); const col1 = { dataIndex: 'light', width: 100 }; const col2 = { dataIndex: 'bamboo', width: 200 }; const col3 = { dataIndex: 'empty', width: 0 }; @@ -55,7 +56,7 @@ describe('Table.FixedHeader', () => { ]); await act(async () => { - jest.runOnlyPendingTimers(); + jest.runAllTimers(); await Promise.resolve(); wrapper.update(); }); @@ -173,7 +174,7 @@ describe('Table.FixedHeader', () => { }, ]); await act(async () => { - jest.runOnlyPendingTimers(); + jest.runAllTimers(); await Promise.resolve(); wrapper.update(); }); @@ -197,7 +198,7 @@ describe('Table.FixedHeader', () => { ]); act(() => { - jest.runOnlyPendingTimers(); + jest.runAllTimers(); wrapper.update(); });