Fix web terminal scroll lag (#1622)
* fix(app): eliminate terminal scroll lag by removing MutationObserver and throttling scroll events The MutationObserver with subtree:true was firing React setState on every xterm DOM mutation (character output, cursor blink, scroll line rendering), causing cascading React re-renders that blocked the main thread and made the mouse wheel unresponsive on web. Changes: - Remove MutationObserver (redundant with ResizeObserver for viewport sizing) - Throttle scroll handler with requestAnimationFrame to batch setState calls - Early-return in updateViewportMetrics when metrics unchanged since last emit - Cancel pending rAF on cleanup to avoid stale updates Co-Authored-By: TommyLike <tommylikehu@gmail.com> * Fix terminal scrollbar metric dedupe state --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
d6b946720e
commit
8228bafc5d
@@ -252,6 +252,7 @@ export default function TerminalEmulator({
|
|||||||
const scrollVisibilityTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const scrollVisibilityTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const scrollActiveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const scrollActiveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastObservedOffsetRef = useRef<number | null>(null);
|
const lastObservedOffsetRef = useRef<number | null>(null);
|
||||||
|
const lastMetricsRef = useRef({ offset: 0, viewportSize: 0, contentSize: 0 });
|
||||||
const themeKey = useMemo(() => buildXtermThemeKey(xtermTheme), [xtermTheme]);
|
const themeKey = useMemo(() => buildXtermThemeKey(xtermTheme), [xtermTheme]);
|
||||||
const xtermThemeRef = useRef(xtermTheme);
|
const xtermThemeRef = useRef(xtermTheme);
|
||||||
xtermThemeRef.current = xtermTheme;
|
xtermThemeRef.current = xtermTheme;
|
||||||
@@ -290,6 +291,19 @@ export default function TerminalEmulator({
|
|||||||
const [isScrollActive, setIsScrollActive] = useState(false);
|
const [isScrollActive, setIsScrollActive] = useState(false);
|
||||||
const [isDropActive, setIsDropActive] = useState(false);
|
const [isDropActive, setIsDropActive] = useState(false);
|
||||||
const dropActiveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const dropActiveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const updateViewportMetricsState = useCallback((metrics: ViewportMetrics) => {
|
||||||
|
const lastMetrics = lastMetricsRef.current;
|
||||||
|
if (
|
||||||
|
metrics.offset === lastMetrics.offset &&
|
||||||
|
metrics.viewportSize === lastMetrics.viewportSize &&
|
||||||
|
metrics.contentSize === lastMetrics.contentSize
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMetricsRef.current = metrics;
|
||||||
|
setViewportMetrics(metrics);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const domBridgeRef = useRef<DOMImperativeFactory | null>(null);
|
const domBridgeRef = useRef<DOMImperativeFactory | null>(null);
|
||||||
useDOMImperativeHandle(
|
useDOMImperativeHandle(
|
||||||
@@ -575,24 +589,30 @@ export default function TerminalEmulator({
|
|||||||
const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport");
|
const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport");
|
||||||
if (!viewportElement) {
|
if (!viewportElement) {
|
||||||
viewportRef.current = null;
|
viewportRef.current = null;
|
||||||
setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 });
|
updateViewportMetricsState({ offset: 0, viewportSize: 0, contentSize: 0 });
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportRef.current = viewportElement;
|
viewportRef.current = viewportElement;
|
||||||
|
|
||||||
const updateViewportMetrics = () => {
|
const updateViewportMetrics = () => {
|
||||||
setViewportMetrics({
|
const offset = Math.max(0, viewportElement.scrollTop);
|
||||||
offset: Math.max(0, viewportElement.scrollTop),
|
const viewportSize = Math.max(0, viewportElement.clientHeight);
|
||||||
viewportSize: Math.max(0, viewportElement.clientHeight),
|
const contentSize = Math.max(0, viewportElement.scrollHeight);
|
||||||
contentSize: Math.max(0, viewportElement.scrollHeight),
|
updateViewportMetricsState({ offset, viewportSize, contentSize });
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateViewportMetrics();
|
updateViewportMetrics();
|
||||||
|
|
||||||
|
let scrollRafId: number | null = null;
|
||||||
const handleViewportScroll = () => {
|
const handleViewportScroll = () => {
|
||||||
updateViewportMetrics();
|
if (scrollRafId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollRafId = requestAnimationFrame(() => {
|
||||||
|
scrollRafId = null;
|
||||||
|
updateViewportMetrics();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
@@ -604,27 +624,20 @@ export default function TerminalEmulator({
|
|||||||
resizeObserver.observe(scrollAreaElement);
|
resizeObserver.observe(scrollAreaElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutationObserver = new MutationObserver(() => {
|
|
||||||
updateViewportMetrics();
|
|
||||||
});
|
|
||||||
mutationObserver.observe(host, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["style", "class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
viewportElement.addEventListener("scroll", handleViewportScroll, { passive: true });
|
viewportElement.addEventListener("scroll", handleViewportScroll, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (scrollRafId !== null) {
|
||||||
|
cancelAnimationFrame(scrollRafId);
|
||||||
|
scrollRafId = null;
|
||||||
|
}
|
||||||
viewportElement.removeEventListener("scroll", handleViewportScroll);
|
viewportElement.removeEventListener("scroll", handleViewportScroll);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
mutationObserver.disconnect();
|
|
||||||
if (viewportRef.current === viewportElement) {
|
if (viewportRef.current === viewportElement) {
|
||||||
viewportRef.current = null;
|
viewportRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [streamKey]);
|
}, [streamKey, updateViewportMetricsState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const maxScrollOffset = Math.max(0, viewportMetrics.contentSize - viewportMetrics.viewportSize);
|
const maxScrollOffset = Math.max(0, viewportMetrics.contentSize - viewportMetrics.viewportSize);
|
||||||
@@ -700,7 +713,7 @@ export default function TerminalEmulator({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
viewportElement.scrollTop = nextOffset;
|
viewportElement.scrollTop = nextOffset;
|
||||||
setViewportMetrics({
|
updateViewportMetricsState({
|
||||||
offset: nextOffset,
|
offset: nextOffset,
|
||||||
viewportSize: Math.max(0, viewportElement.clientHeight),
|
viewportSize: Math.max(0, viewportElement.clientHeight),
|
||||||
contentSize: Math.max(0, viewportElement.scrollHeight),
|
contentSize: Math.max(0, viewportElement.scrollHeight),
|
||||||
@@ -720,7 +733,12 @@ export default function TerminalEmulator({
|
|||||||
window.removeEventListener("pointerup", stopDragging);
|
window.removeEventListener("pointerup", stopDragging);
|
||||||
window.removeEventListener("pointercancel", stopDragging);
|
window.removeEventListener("pointercancel", stopDragging);
|
||||||
};
|
};
|
||||||
}, [isDraggingScrollbar, scrollbarGeometry.maxHandleOffset, scrollbarGeometry.maxScrollOffset]);
|
}, [
|
||||||
|
isDraggingScrollbar,
|
||||||
|
scrollbarGeometry.maxHandleOffset,
|
||||||
|
scrollbarGeometry.maxScrollOffset,
|
||||||
|
updateViewportMetricsState,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleVisible =
|
const handleVisible =
|
||||||
scrollbarGeometry.isVisible && (isDraggingScrollbar || isScrollVisible || isHandleHovered);
|
scrollbarGeometry.isVisible && (isDraggingScrollbar || isScrollVisible || isHandleHovered);
|
||||||
|
|||||||
Reference in New Issue
Block a user