Skip to content

Commit 052b2ae

Browse files
authored
perf: Not render fully Popup on init (#540)
* chore: fix demo * chore: perf before render * chore: perf before render * test: add test case
1 parent e2e2a9c commit 052b2ae

File tree

3 files changed

+178
-73
lines changed

3 files changed

+178
-73
lines changed

docs/examples/simple.tsx

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint no-console:0 */
22

3-
import Trigger, { ActionType } from '@rc-component/trigger';
3+
import Trigger, { type ActionType } from '@rc-component/trigger';
44
import React from 'react';
55
import '../../assets/index.less';
66

@@ -43,29 +43,26 @@ function getPopupContainer(trigger) {
4343
return trigger.parentNode;
4444
}
4545

46-
const InnerTarget = (props) => (
47-
<div
48-
style={{
49-
margin: 20,
50-
display: 'inline-block',
51-
background: 'rgba(255, 0, 0, 0.05)',
52-
}}
53-
tabIndex={0}
54-
role="button"
55-
{...props}
56-
>
57-
<p>This is a example of trigger usage.</p>
58-
<p>You can adjust the value above</p>
59-
<p>which will also change the behaviour of popup.</p>
60-
</div>
46+
const InnerTarget = React.forwardRef(
47+
(props: any, ref: React.Ref<HTMLElement>) => (
48+
<div
49+
style={{
50+
margin: 20,
51+
display: 'inline-block',
52+
background: 'rgba(255, 0, 0, 0.05)',
53+
}}
54+
tabIndex={0}
55+
role="button"
56+
{...props}
57+
ref={ref}
58+
>
59+
<p>This is a example of trigger usage.</p>
60+
<p>You can adjust the value above</p>
61+
<p>which will also change the behavior of popup.</p>
62+
</div>
63+
),
6164
);
6265

63-
const RefTarget = React.forwardRef((props, ref) => {
64-
React.useImperativeHandle(ref, () => ({}));
65-
66-
return <InnerTarget {...props} />;
67-
});
68-
6966
interface TestState {
7067
mask: boolean;
7168
maskClosable: boolean;
@@ -88,7 +85,7 @@ interface TestState {
8885

8986
class Test extends React.Component<any, TestState> {
9087
state: TestState = {
91-
mask: true,
88+
mask: false,
9289
maskClosable: true,
9390
placement: 'bottom',
9491
trigger: {
@@ -377,7 +374,7 @@ class Test extends React.Component<any, TestState> {
377374
motionName: state.transitionName,
378375
}}
379376
>
380-
<RefTarget />
377+
<InnerTarget />
381378
</Trigger>
382379
</div>
383380
</div>

src/index.tsx

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,10 @@ export function generateTrigger(
595595
};
596596
}
597597

598+
// ============================ Perf ============================
599+
const rendedRef = React.useRef(false);
600+
rendedRef.current ||= forceRender || mergedOpen || inMotion;
601+
598602
// =========================== Render ===========================
599603
const mergedChildrenProps = {
600604
...originChildProps,
@@ -651,55 +655,57 @@ export function generateTrigger(
651655
>
652656
{triggerNode}
653657
</ResizeObserver>
654-
<TriggerContext.Provider value={context}>
655-
<Popup
656-
portal={PortalComponent}
657-
ref={setPopupRef}
658-
prefixCls={prefixCls}
659-
popup={popup}
660-
className={classNames(popupClassName, alignedClassName)}
661-
style={popupStyle}
662-
target={targetEle}
663-
onMouseEnter={onPopupMouseEnter}
664-
onMouseLeave={onPopupMouseLeave}
665-
// https://github.com/ant-design/ant-design/issues/43924
666-
onPointerEnter={onPopupMouseEnter}
667-
zIndex={zIndex}
668-
// Open
669-
open={mergedOpen}
670-
keepDom={inMotion}
671-
fresh={fresh}
672-
// Click
673-
onClick={onPopupClick}
674-
onPointerDownCapture={onPopupPointerDown}
675-
// Mask
676-
mask={mask}
677-
// Motion
678-
motion={popupMotion}
679-
maskMotion={maskMotion}
680-
onVisibleChanged={onVisibleChanged}
681-
onPrepare={onPrepare}
682-
// Portal
683-
forceRender={forceRender}
684-
autoDestroy={mergedAutoDestroy}
685-
getPopupContainer={getPopupContainer}
686-
// Arrow
687-
align={alignInfo}
688-
arrow={innerArrow}
689-
arrowPos={arrowPos}
690-
// Align
691-
ready={ready}
692-
offsetX={offsetX}
693-
offsetY={offsetY}
694-
offsetR={offsetR}
695-
offsetB={offsetB}
696-
onAlign={triggerAlign}
697-
// Stretch
698-
stretch={stretch}
699-
targetWidth={targetWidth / scaleX}
700-
targetHeight={targetHeight / scaleY}
701-
/>
702-
</TriggerContext.Provider>
658+
{rendedRef.current && (
659+
<TriggerContext.Provider value={context}>
660+
<Popup
661+
portal={PortalComponent}
662+
ref={setPopupRef}
663+
prefixCls={prefixCls}
664+
popup={popup}
665+
className={classNames(popupClassName, alignedClassName)}
666+
style={popupStyle}
667+
target={targetEle}
668+
onMouseEnter={onPopupMouseEnter}
669+
onMouseLeave={onPopupMouseLeave}
670+
// https://github.com/ant-design/ant-design/issues/43924
671+
onPointerEnter={onPopupMouseEnter}
672+
zIndex={zIndex}
673+
// Open
674+
open={mergedOpen}
675+
keepDom={inMotion}
676+
fresh={fresh}
677+
// Click
678+
onClick={onPopupClick}
679+
onPointerDownCapture={onPopupPointerDown}
680+
// Mask
681+
mask={mask}
682+
// Motion
683+
motion={popupMotion}
684+
maskMotion={maskMotion}
685+
onVisibleChanged={onVisibleChanged}
686+
onPrepare={onPrepare}
687+
// Portal
688+
forceRender={forceRender}
689+
autoDestroy={mergedAutoDestroy}
690+
getPopupContainer={getPopupContainer}
691+
// Arrow
692+
align={alignInfo}
693+
arrow={innerArrow}
694+
arrowPos={arrowPos}
695+
// Align
696+
ready={ready}
697+
offsetX={offsetX}
698+
offsetY={offsetY}
699+
offsetR={offsetR}
700+
offsetB={offsetB}
701+
onAlign={triggerAlign}
702+
// Stretch
703+
stretch={stretch}
704+
targetWidth={targetWidth / scaleX}
705+
targetHeight={targetHeight / scaleY}
706+
/>
707+
</TriggerContext.Provider>
708+
)}
703709
</>
704710
);
705711
});

tests/perf.test.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { cleanup, fireEvent, render } from '@testing-library/react';
2+
import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
3+
import React from 'react';
4+
import Trigger, { type TriggerProps } from '../src';
5+
import { awaitFakeTimer, placementAlignMap } from './util';
6+
7+
jest.mock('../src/Popup', () => {
8+
const OriReact = jest.requireActual('react');
9+
const OriPopup = jest.requireActual('../src/Popup').default;
10+
11+
return OriReact.forwardRef((props, ref) => {
12+
global.popupCalledTimes = (global.popupCalledTimes || 0) + 1;
13+
return <OriPopup {...props} ref={ref} />;
14+
});
15+
});
16+
17+
describe('Trigger.Basic', () => {
18+
beforeAll(() => {
19+
spyElementPrototypes(HTMLElement, {
20+
offsetParent: {
21+
get: () => document.body,
22+
},
23+
});
24+
});
25+
26+
beforeEach(() => {
27+
global.popupCalledTimes = 0;
28+
jest.useFakeTimers();
29+
});
30+
31+
afterEach(() => {
32+
cleanup();
33+
jest.useRealTimers();
34+
});
35+
36+
async function trigger(dom: HTMLElement, selector: string, method = 'click') {
37+
fireEvent[method](dom.querySelector(selector));
38+
await awaitFakeTimer();
39+
}
40+
41+
const renderTrigger = (props?: Partial<TriggerProps>) => (
42+
<Trigger
43+
action={['click']}
44+
popupAlign={placementAlignMap.left}
45+
popup={<strong className="x-content">tooltip2</strong>}
46+
{...props}
47+
>
48+
<div className="target">click</div>
49+
</Trigger>
50+
);
51+
52+
describe('Performance', () => {
53+
it('not create Popup when !open', async () => {
54+
const { container } = render(renderTrigger());
55+
56+
// Not render Popup
57+
await awaitFakeTimer();
58+
expect(global.popupCalledTimes).toBe(0);
59+
60+
// Now can render Popup
61+
await trigger(container, '.target');
62+
expect(global.popupCalledTimes).toBeGreaterThan(0);
63+
64+
expect(document.querySelector('.rc-trigger-popup')).toBeTruthy();
65+
});
66+
67+
it('forceRender should create when !open', async () => {
68+
const { container } = render(
69+
renderTrigger({
70+
forceRender: true,
71+
}),
72+
);
73+
74+
await awaitFakeTimer();
75+
await trigger(container, '.target');
76+
expect(global.popupCalledTimes).toBeGreaterThan(0);
77+
78+
expect(document.querySelector('.rc-trigger-popup')).toBeTruthy();
79+
});
80+
81+
it('hide should keep render Popup', async () => {
82+
const { rerender } = render(
83+
renderTrigger({
84+
popupVisible: true,
85+
}),
86+
);
87+
88+
await awaitFakeTimer();
89+
expect(global.popupCalledTimes).toBeGreaterThan(0);
90+
91+
// Hide
92+
global.popupCalledTimes = 0;
93+
rerender(
94+
renderTrigger({
95+
popupVisible: false,
96+
}),
97+
);
98+
await awaitFakeTimer();
99+
expect(global.popupCalledTimes).toBeGreaterThan(0);
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)