Skip to content

Commit 5726a87

Browse files
JacksonGLfacebook-github-bot
authored andcommitted
chore(lens): debounce react component stack panel update to avoid UI flickering
Summary: This diff debounces updates to the React component stack panel to prevent a flickering loop caused by rapid mouse transitions. The issue occurs when the mouse leaves leak outline A and enters the React component stack, which causes the stack to disappear due to the outline change. If leak outline B is underneath the stack, it triggers a re-render of the stack. This causes the mouse out to be out of B (because the react component stack is rendered on top of it) and back into the react component stack panel, this causes the same <leaving outline, enter component stack, component stack disappear, re-enter outline> behavior to repeat, resulting in an infinite loop of UI flickering. Reviewed By: twobassdrum Differential Revision: D73486515 fbshipit-source-id: 523c5b0f821c1c60f774270ab26d988732cc6a0b
1 parent 5455ec8 commit 5726a87

File tree

3 files changed

+115
-33
lines changed

3 files changed

+115
-33
lines changed

packages/lens/src/visual/components/component-stack-panel.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
RegisterDataUpdateCallback,
1212
VisualizerData,
1313
} from '../dom-element-visualizer-interactive';
14-
import {createVisualizerElement} from '../visual-utils';
14+
import {createVisualizerElement, debounce} from '../visual-utils';
1515

1616
export function createComponentStackPanel(
1717
registerDataUpdateCallback: RegisterDataUpdateCallback,
@@ -34,37 +34,52 @@ export function createComponentStackPanel(
3434
panel.style.color = 'white';
3535
panel.id = 'memory-visualization-component-stack-panel';
3636

37+
let pinned = false;
38+
39+
panel.addEventListener('mouseenter', () => {
40+
pinned = true;
41+
});
42+
43+
panel.addEventListener('mouseleave', () => {
44+
pinned = false;
45+
});
46+
3747
// Register data update callback to update component stack panel
38-
registerDataUpdateCallback((data: VisualizerData) => {
39-
panel.style.display = data.selectedElementId != null ? 'flex' : 'none';
40-
panel.innerHTML = '';
48+
registerDataUpdateCallback(
49+
debounce((data: VisualizerData) => {
50+
if (pinned) {
51+
return;
52+
}
53+
panel.style.display = data.selectedElementId != null ? 'flex' : 'none';
54+
panel.innerHTML = '';
4155

42-
if (
43-
data.selectedElementId == null ||
44-
!data.selectedReactComponentStack?.length
45-
) {
46-
return;
47-
}
56+
if (
57+
data.selectedElementId == null ||
58+
!data.selectedReactComponentStack?.length
59+
) {
60+
return;
61+
}
4862

49-
const title = createVisualizerElement('div');
50-
title.textContent = 'Component Stack:';
51-
title.style.fontWeight = 'bold';
52-
title.style.marginBottom = '8px';
53-
panel.appendChild(title);
63+
const title = createVisualizerElement('div');
64+
title.textContent = 'Component Stack:';
65+
title.style.fontWeight = 'bold';
66+
title.style.marginBottom = '8px';
67+
panel.appendChild(title);
5468

55-
let actualComponentStackLength = 0;
56-
data.selectedReactComponentStack.forEach((component: string) => {
57-
const componentDiv = createVisualizerElement('div');
58-
componentDiv.style.marginBottom = '4px';
59-
componentDiv.textContent = component;
60-
panel.appendChild(componentDiv);
61-
++actualComponentStackLength;
62-
});
69+
let actualComponentStackLength = 0;
70+
data.selectedReactComponentStack.forEach((component: string) => {
71+
const componentDiv = createVisualizerElement('div');
72+
componentDiv.style.marginBottom = '4px';
73+
componentDiv.textContent = component;
74+
panel.appendChild(componentDiv);
75+
++actualComponentStackLength;
76+
});
6377

64-
if (actualComponentStackLength === 0) {
65-
title.remove();
66-
}
67-
});
78+
if (actualComponentStackLength === 0) {
79+
title.remove();
80+
}
81+
}, 1),
82+
);
6883

6984
return panel;
7085
}

packages/lens/src/visual/components/overlay-rectangle.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
*/
1010
import type {AnyValue, DOMElementInfo} from '../../core/types';
1111
import {IntersectionObserverManager} from '../../utils/intersection-observer';
12-
import {createVisualizerElement} from '../visual-utils';
12+
import {
13+
createVisualizerElement,
14+
addTrackedListener,
15+
removeAllListeners,
16+
} from '../visual-utils';
1317

1418
type OutlineState = {pinned: boolean; selected: boolean};
1519

@@ -64,28 +68,31 @@ export function createOverlayRectangle(
6468

6569
const componentStack = info.componentStack ?? [];
6670
const componentName = componentStack[0] ?? '';
67-
const elementIdStr = `memory-id-${elementId}@`;
6871

6972
let pinned = false;
7073
let selected = false;
7174

7275
const divRef = new WeakRef(div);
7376

74-
div.addEventListener('mouseover', () => {
77+
addTrackedListener(divRef, 'mouseover', () => {
78+
// note that elementIdStr should not be genearated in the
79+
// inside the function scope of createOverlayRectangle
80+
// to avoid unnecessary retainer path in the heap snapshot
81+
const elementIdStr = `memory-id-${elementId}@`;
7582
labelDiv.remove();
76-
div.appendChild(labelDiv);
83+
divRef.deref()?.appendChild(labelDiv);
7784
labelDiv.textContent = `${componentName} (${elementIdStr})`;
7885
labelDiv.style.display = 'inline-block';
7986
setSelectedId(elementId);
8087
});
8188

82-
div.addEventListener('mouseout', () => {
89+
addTrackedListener(divRef, 'mouseout', () => {
8390
labelDiv.style.display = 'none';
8491
labelDiv.remove();
8592
setUnSelectedId(elementId);
8693
});
8794

88-
div.addEventListener('click', () => {
95+
addTrackedListener(divRef, 'click', () => {
8996
setClickedId(elementId);
9097
});
9198

@@ -122,6 +129,7 @@ export function createOverlayRectangle(
122129
if (div == null) {
123130
return;
124131
}
132+
removeAllListeners(divRef);
125133
observerManager.unobserve(divRef);
126134
(div as AnyValue).__cleanup = null;
127135
(div as AnyValue).__selected = null;

packages/lens/src/visual/visual-utils.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
* @format
88
* @oncall memory_lab
99
*/
10+
import type {AnyValue} from '../core/types';
11+
1012
const VISUALIZER_DATA_ATTR = 'data-visualizer';
1113

1214
function setVisualizerElement(element: Element) {
1315
element.setAttribute(VISUALIZER_DATA_ATTR, 'true');
16+
element.setAttribute('data-visualcompletion', 'ignore');
1417
}
1518

1619
export function isVisualizerElement(element: Element): boolean {
@@ -28,3 +31,59 @@ export function tryToAttachOverlay(overlayDiv: HTMLDivElement) {
2831
document.body.appendChild(overlayDiv);
2932
}
3033
}
34+
35+
type EventListenerEntry = {
36+
type: string;
37+
cb: EventListenerOrEventListenerObject;
38+
options?: boolean | AddEventListenerOptions;
39+
};
40+
41+
const listenerMap = new WeakMap<EventTarget, EventListenerEntry[]>();
42+
43+
export function addTrackedListener(
44+
elRef: WeakRef<EventTarget>,
45+
type: string,
46+
cb: EventListenerOrEventListenerObject,
47+
options?: boolean | AddEventListenerOptions,
48+
): void {
49+
const el = elRef.deref();
50+
if (!el) return;
51+
52+
el.addEventListener(type, cb, options);
53+
54+
if (!listenerMap.has(el)) {
55+
listenerMap.set(el, []);
56+
}
57+
58+
listenerMap.get(el)?.push({type, cb, options});
59+
}
60+
61+
export function removeAllListeners(elRef: WeakRef<EventTarget>): void {
62+
const el = elRef.deref();
63+
if (!el) return;
64+
65+
const listeners = listenerMap.get(el);
66+
if (!listeners) return;
67+
68+
for (const {type, cb, options} of listeners) {
69+
el.removeEventListener(type, cb, options);
70+
}
71+
72+
listenerMap.delete(el);
73+
}
74+
75+
export function debounce<T extends (...args: AnyValue[]) => AnyValue>(
76+
callback: T,
77+
delay: number,
78+
): (...args: Parameters<T>) => void {
79+
let timer: ReturnType<typeof setTimeout> | null = null;
80+
81+
return (...args: Parameters<T>) => {
82+
if (timer) {
83+
clearTimeout(timer);
84+
}
85+
timer = setTimeout(() => {
86+
callback(...args);
87+
}, delay);
88+
};
89+
}

0 commit comments

Comments
 (0)