Skip to content

Commit 0dfea51

Browse files
authored
perf: not render popup on init (#554)
* perf: not render popup on init * tests: perf not render init
1 parent 97eddcc commit 0dfea51

File tree

2 files changed

+157
-49
lines changed

2 files changed

+157
-49
lines changed

src/index.tsx

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,10 @@ export function generateTrigger(
644644
cloneProps.className = classNames(originChildProps.className, className);
645645
}
646646

647+
// ============================ Perf ============================
648+
const renderedRef = React.useRef(false);
649+
renderedRef.current ||= forceRender || mergedOpen || inMotion;
650+
647651
// =========================== Render ===========================
648652
const mergedChildrenProps = {
649653
...originChildProps,
@@ -702,55 +706,57 @@ export function generateTrigger(
702706
{triggerNode}
703707
</TriggerWrapper>
704708
</ResizeObserver>
705-
<TriggerContext.Provider value={context}>
706-
<Popup
707-
portal={PortalComponent}
708-
ref={setPopupRef}
709-
prefixCls={prefixCls}
710-
popup={popup}
711-
className={classNames(popupClassName, alignedClassName)}
712-
style={popupStyle}
713-
target={targetEle}
714-
onMouseEnter={onPopupMouseEnter}
715-
onMouseLeave={onPopupMouseLeave}
716-
// https://github.com/ant-design/ant-design/issues/43924
717-
onPointerEnter={onPopupMouseEnter}
718-
zIndex={zIndex}
719-
// Open
720-
open={mergedOpen}
721-
keepDom={inMotion}
722-
fresh={fresh}
723-
// Click
724-
onClick={onPopupClick}
725-
onPointerDownCapture={onPopupPointerDown}
726-
// Mask
727-
mask={mask}
728-
// Motion
729-
motion={mergePopupMotion}
730-
maskMotion={mergeMaskMotion}
731-
onVisibleChanged={onVisibleChanged}
732-
onPrepare={onPrepare}
733-
// Portal
734-
forceRender={forceRender}
735-
autoDestroy={mergedAutoDestroy}
736-
getPopupContainer={getPopupContainer}
737-
// Arrow
738-
align={alignInfo}
739-
arrow={innerArrow}
740-
arrowPos={arrowPos}
741-
// Align
742-
ready={ready}
743-
offsetX={offsetX}
744-
offsetY={offsetY}
745-
offsetR={offsetR}
746-
offsetB={offsetB}
747-
onAlign={triggerAlign}
748-
// Stretch
749-
stretch={stretch}
750-
targetWidth={targetWidth / scaleX}
751-
targetHeight={targetHeight / scaleY}
752-
/>
753-
</TriggerContext.Provider>
709+
{renderedRef.current && (
710+
<TriggerContext.Provider value={context}>
711+
<Popup
712+
portal={PortalComponent}
713+
ref={setPopupRef}
714+
prefixCls={prefixCls}
715+
popup={popup}
716+
className={classNames(popupClassName, alignedClassName)}
717+
style={popupStyle}
718+
target={targetEle}
719+
onMouseEnter={onPopupMouseEnter}
720+
onMouseLeave={onPopupMouseLeave}
721+
// https://github.com/ant-design/ant-design/issues/43924
722+
onPointerEnter={onPopupMouseEnter}
723+
zIndex={zIndex}
724+
// Open
725+
open={mergedOpen}
726+
keepDom={inMotion}
727+
fresh={fresh}
728+
// Click
729+
onClick={onPopupClick}
730+
onPointerDownCapture={onPopupPointerDown}
731+
// Mask
732+
mask={mask}
733+
// Motion
734+
motion={mergePopupMotion}
735+
maskMotion={mergeMaskMotion}
736+
onVisibleChanged={onVisibleChanged}
737+
onPrepare={onPrepare}
738+
// Portal
739+
forceRender={forceRender}
740+
autoDestroy={mergedAutoDestroy}
741+
getPopupContainer={getPopupContainer}
742+
// Arrow
743+
align={alignInfo}
744+
arrow={innerArrow}
745+
arrowPos={arrowPos}
746+
// Align
747+
ready={ready}
748+
offsetX={offsetX}
749+
offsetY={offsetY}
750+
offsetR={offsetR}
751+
offsetB={offsetB}
752+
onAlign={triggerAlign}
753+
// Stretch
754+
stretch={stretch}
755+
targetWidth={targetWidth / scaleX}
756+
targetHeight={targetHeight / scaleY}
757+
/>
758+
</TriggerContext.Provider>
759+
)}
754760
</>
755761
);
756762
});

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-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)