Skip to content

Commit fcad52e

Browse files
authored
perf: optimize widget header updates and debounce (#150)
1 parent e438697 commit fcad52e

File tree

2 files changed

+153
-99
lines changed

2 files changed

+153
-99
lines changed
Lines changed: 119 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,74 @@
1-
import { useRef, useEffect, useCallback, useState } from "preact/hooks";
1+
import { useRef, useEffect, useCallback } from "preact/hooks";
22
import { getDisplayName } from 'bippy';
3-
import { cn } from "@web-utils/helpers";
43
import { Store } from "../../../..";
54
import { getCompositeComponentFromElement, getOverrideMethods } from "../../inspect-element/utils";
65
import { replayComponent } from "../../inspect-element/view-state";
76
import { Icon } from "../icon";
7+
import { debounce } from "../../utils/helpers";
88

9-
export const Header = () => {
10-
const inspectState = Store.inspectState.value;
11-
const refComponentName = useRef<HTMLSpanElement>(null);
12-
const refMetrics = useRef<HTMLSpanElement>(null);
13-
const [isReplaying, setIsReplaying] = useState(false);
14-
15-
useEffect(() => {
16-
const updateMetrics = () => {
17-
if (!refComponentName.current || !refMetrics.current) return;
18-
if (inspectState.kind !== 'focused') return;
19-
20-
if (!document.contains(inspectState.focusedDomElement)) {
21-
if (Store.inspectState.value.propContainer) {
22-
Store.inspectState.value = {
23-
kind: 'inspect-off',
24-
propContainer: Store.inspectState.value.propContainer,
25-
};
26-
}
27-
return;
28-
}
9+
const BtnReplay = () => {
10+
const refBtnReplay = useRef<HTMLButtonElement>(null);
11+
const refIsReplaying = useRef(false);
12+
const timeoutRef = useRef<number>();
2913

14+
const { overrideProps, overrideHookState } = getOverrideMethods();
15+
const canEdit = !overrideProps;
3016

31-
const { parentCompositeFiber } = getCompositeComponentFromElement(inspectState.focusedDomElement);
17+
const handleReplay = useCallback((e: MouseEvent) => {
18+
e.stopPropagation();
3219

33-
if (!parentCompositeFiber) return;
20+
const inspectState = Store.inspectState.value;
21+
if (refIsReplaying.current || inspectState.kind !== 'focused') return;
3422

35-
const reportDataFiber =
36-
Store.reportData.get(parentCompositeFiber) ??
37-
(parentCompositeFiber.alternate
38-
? Store.reportData.get(parentCompositeFiber.alternate)
39-
: null);
23+
const { parentCompositeFiber } = getCompositeComponentFromElement(inspectState.focusedDomElement);
24+
if (!parentCompositeFiber || !overrideProps || !overrideHookState) return;
4025

41-
const componentName = getDisplayName(parentCompositeFiber.type) ?? 'Unknown';
26+
refIsReplaying.current = true;
27+
refBtnReplay.current?.classList.add('disabled');
4228

43-
const renderCount = reportDataFiber?.count ?? 0;
44-
const renderTime = reportDataFiber?.time ?? 0;
29+
void replayComponent(parentCompositeFiber).finally(() => {
30+
if (timeoutRef.current) {
31+
clearTimeout(timeoutRef.current);
32+
}
4533

46-
refComponentName.current.textContent = componentName;
47-
refMetrics.current.textContent = renderCount > 0
48-
? `${renderCount} renders${renderTime > 0 ? ` • ${renderTime.toFixed(2)}ms` : ''}`
49-
: '';
50-
};
34+
const cleanup = () => {
35+
refIsReplaying.current = false;
36+
refBtnReplay.current?.classList.remove('disabled');
37+
};
5138

52-
const unsubscribe = Store.lastReportTime.subscribe(updateMetrics);
39+
if (document.hidden) {
40+
cleanup();
41+
} else {
42+
timeoutRef.current = window.setTimeout(cleanup, 300);
43+
}
44+
});
45+
}, []);
5346

47+
useEffect(() => {
5448
return () => {
55-
unsubscribe();
49+
if (timeoutRef.current) {
50+
clearTimeout(timeoutRef.current);
51+
}
5652
};
57-
}, [inspectState]);
53+
}, []);
54+
55+
if (!canEdit) return null;
56+
57+
return (
58+
<button
59+
ref={refBtnReplay}
60+
title="Replay component"
61+
className="react-scan-replay-button"
62+
onClick={handleReplay}
63+
>
64+
<Icon name="icon-replay" />
65+
</button>
66+
);
67+
};
68+
69+
export const Header = () => {
70+
const refComponentName = useRef<HTMLSpanElement>(null);
71+
const refMetrics = useRef<HTMLSpanElement>(null);
5872

5973
const handleClose = useCallback(() => {
6074
if (Store.inspectState.value.propContainer) {
@@ -65,64 +79,79 @@ export const Header = () => {
6579
}
6680
}, []);
6781

68-
const handleReplay = useCallback((e: MouseEvent) => {
69-
void (async () => {
70-
e.stopPropagation();
71-
if (isReplaying || inspectState.kind !== 'focused') return;
82+
const updateHeaderContent = useCallback(() => {
83+
const inspectState = Store.inspectState.value;
84+
if (inspectState.kind !== 'focused') return;
85+
86+
const focusedDomElement = inspectState.focusedDomElement;
87+
if (!refComponentName.current || !refMetrics.current || !focusedDomElement) return;
88+
89+
const { parentCompositeFiber } = getCompositeComponentFromElement(focusedDomElement);
90+
if (!parentCompositeFiber) return;
7291

73-
const { parentCompositeFiber } = getCompositeComponentFromElement(inspectState.focusedDomElement);
74-
if (!parentCompositeFiber) return;
92+
const currentComponentName = refComponentName.current.dataset.text;
93+
const currentMetrics = refMetrics.current.dataset.text;
7594

76-
const { overrideProps, overrideHookState } = getOverrideMethods();
77-
if (!overrideProps || !overrideHookState) return;
95+
const fiber = parentCompositeFiber.alternate ?? parentCompositeFiber;
96+
const reportData = Store.reportData.get(fiber);
97+
const componentName = getDisplayName(parentCompositeFiber.type) ?? 'Unknown';
7898

79-
setIsReplaying(true);
99+
if (componentName === currentComponentName && reportData?.count === 0) {
100+
return;
101+
}
80102

81-
try {
82-
await replayComponent(parentCompositeFiber);
83-
} finally {
84-
setTimeout(() => {
85-
setIsReplaying(false);
86-
}, 300);
103+
const renderCount = reportData?.count ?? 0;
104+
const renderTime = reportData?.time ?? 0;
105+
const newMetrics = renderCount > 0
106+
? `${renderCount} renders${renderTime > 0 ? ` • ${renderTime.toFixed(2)}ms` : ''}`
107+
: '';
108+
109+
if (componentName !== currentComponentName || newMetrics !== currentMetrics) {
110+
requestAnimationFrame(() => {
111+
if (!refComponentName.current || !refMetrics.current) return;
112+
refComponentName.current.dataset.text = componentName;
113+
refMetrics.current.dataset.text = newMetrics;
114+
});
115+
}
116+
}, []);
117+
118+
useEffect(() => {
119+
const debouncedUpdate = debounce(updateHeaderContent, 16, { leading: true });
120+
121+
const unsubscribeLastReportTime = Store.lastReportTime.subscribe(debouncedUpdate);
122+
const unsubscribeStoreInspectState = Store.inspectState.subscribe(state => {
123+
if (state.kind === 'focused') {
124+
debouncedUpdate();
87125
}
88-
})();
89-
}, [inspectState, isReplaying]);
126+
});
90127

91-
const { overrideProps } = getOverrideMethods();
92-
const canEdit = !!overrideProps;
128+
return () => {
129+
unsubscribeLastReportTime();
130+
unsubscribeStoreInspectState();
131+
debouncedUpdate.cancel?.();
132+
};
133+
}, [updateHeaderContent]);
93134

94135
return (
95-
<div
96-
className={cn(
97-
"react-scan-header",
98-
"flex",
99-
"min-h-9",
100-
'whitespace-nowrap',
101-
"overflow-hidden",
102-
)}
103-
>
104-
<div className="react-scan-header-left overflow-hidden">
105-
<span ref={refComponentName} className="react-scan-component-name" />
106-
<span ref={refMetrics} className="react-scan-metrics" />
107-
</div>
108-
<div class="react-scan-header-right">
109-
{canEdit && (
110-
<button
111-
title="Replay component"
112-
class={`react-scan-replay-button${isReplaying ? ' disabled' : ''}`}
113-
onClick={handleReplay}
114-
>
115-
<Icon name="icon-replay" />
116-
</button>
117-
)}
118-
<button
119-
title="Close"
120-
class="react-scan-close-button"
121-
onClick={handleClose}
122-
>
123-
<Icon name="icon-close" />
124-
</button>
125-
</div>
136+
<div className="react-scan-header">
137+
<span
138+
ref={refComponentName}
139+
className="with-data-text"
140+
/>
141+
<span
142+
ref={refMetrics}
143+
className="with-data-text mr-auto !overflow-visible text-xs text-[#888]"
144+
/>
145+
146+
<BtnReplay />
147+
148+
<button
149+
title="Close"
150+
class="react-scan-close-button"
151+
onClick={handleClose}
152+
>
153+
<Icon name="icon-close" />
154+
</button>
126155
</div>
127156
);
128157
};

packages/scan/src/core/web/utils/helpers.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,48 @@ export const throttle = <T extends (...args: Array<any>) => any>(
3737
};
3838
};
3939

40-
41-
export const debounce = <T extends (...args: Array<any>) => void>(
40+
export const debounce = <T extends (...args: Array<any>) => any>(
4241
fn: T,
43-
delay: number
42+
wait: number,
43+
options: { leading?: boolean; trailing?: boolean } = {}
4444
) => {
45-
let timeoutId: number;
45+
let timeoutId: number | undefined;
46+
let lastArgs: Parameters<T> | undefined;
47+
let isLeadingInvoked = false;
4648

4749
const debounced = (...args: Parameters<T>) => {
48-
window.clearTimeout(timeoutId);
49-
timeoutId = window.setTimeout(() => fn(...args), delay);
50+
lastArgs = args;
51+
52+
if (options.leading && !isLeadingInvoked) {
53+
isLeadingInvoked = true;
54+
fn(...args);
55+
return;
56+
}
57+
58+
if (timeoutId !== undefined) {
59+
clearTimeout(timeoutId);
60+
}
61+
62+
if (options.trailing !== false) {
63+
timeoutId = window.setTimeout(() => {
64+
isLeadingInvoked = false;
65+
timeoutId = undefined;
66+
fn(...(lastArgs!));
67+
}, wait);
68+
}
5069
};
5170

52-
debounced.cancel = () => window.clearTimeout(timeoutId);
71+
debounced.cancel = () => {
72+
if (timeoutId !== undefined) {
73+
clearTimeout(timeoutId);
74+
timeoutId = undefined;
75+
isLeadingInvoked = false;
76+
lastArgs = undefined;
77+
}
78+
};
5379

5480
return debounced;
55-
}
56-
81+
};
5782

5883
export const isOutlineUnstable = (outline: PendingOutline) => {
5984
for (let i = 0, len = outline.renders.length; i < len; i++) {

0 commit comments

Comments
 (0)