perf: batch mobile keyboard viewport handlers with rAF and debounce (#660)
Reduce layout thrashing during virtual keyboard open/close on mobile by batching resize and visualViewport event handlers to once per animation frame. No visual or behavioral changes — desktop is unaffected. - MainLayout: rAF-batch visualViewport resize/scroll and window resize listeners (was firing 10-20x per frame during keyboard animation) - Header: rAF-batch ResizeObserver and resize/orientationchange callbacks - ChatContainer: rAF-batch ResizeObserver and resize fallback callbacks - useDeviceInfo: 150ms debounce on resize listener (device type does not change during keyboard transitions)
This commit is contained in:
@@ -281,17 +281,28 @@ export const ChatContainer: React.FC = () => {
|
||||
|
||||
updateChatScrollHeight();
|
||||
|
||||
let rafId = 0;
|
||||
const scheduleUpdate = () => {
|
||||
if (rafId) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = 0;
|
||||
updateChatScrollHeight();
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
window.addEventListener('resize', updateChatScrollHeight);
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateChatScrollHeight);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
window.removeEventListener('resize', scheduleUpdate);
|
||||
};
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateChatScrollHeight);
|
||||
const resizeObserver = new ResizeObserver(scheduleUpdate);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [currentSessionId, isDesktopExpandedInput, scrollRef]);
|
||||
|
||||
@@ -779,18 +779,26 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
return () => { };
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
updateHeaderHeight();
|
||||
});
|
||||
let rafId = 0;
|
||||
const scheduleUpdate = () => {
|
||||
if (rafId) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = 0;
|
||||
updateHeaderHeight();
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(scheduleUpdate);
|
||||
|
||||
observer.observe(node);
|
||||
window.addEventListener('resize', updateHeaderHeight);
|
||||
window.addEventListener('orientationchange', updateHeaderHeight);
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
window.addEventListener('orientationchange', scheduleUpdate);
|
||||
|
||||
return () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', updateHeaderHeight);
|
||||
window.removeEventListener('orientationchange', updateHeaderHeight);
|
||||
window.removeEventListener('resize', scheduleUpdate);
|
||||
window.removeEventListener('orientationchange', scheduleUpdate);
|
||||
};
|
||||
}, [updateHeaderHeight]);
|
||||
|
||||
|
||||
@@ -378,6 +378,10 @@ export const MainLayout: React.FC = () => {
|
||||
setKeyboardOpen(false);
|
||||
};
|
||||
|
||||
// Batch visualViewport updates to once per animation frame to avoid
|
||||
// layout thrashing during keyboard open/close animations.
|
||||
let rafId = 0;
|
||||
|
||||
const updateVisualViewport = () => {
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
@@ -480,13 +484,21 @@ export const MainLayout: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleVisualViewportUpdate = () => {
|
||||
if (rafId) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = 0;
|
||||
updateVisualViewport();
|
||||
});
|
||||
};
|
||||
|
||||
updateVisualViewport();
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
viewport?.addEventListener('resize', updateVisualViewport);
|
||||
viewport?.addEventListener('scroll', updateVisualViewport);
|
||||
window.addEventListener('resize', updateVisualViewport);
|
||||
window.addEventListener('orientationchange', updateVisualViewport);
|
||||
viewport?.addEventListener('resize', scheduleVisualViewportUpdate);
|
||||
viewport?.addEventListener('scroll', scheduleVisualViewportUpdate);
|
||||
window.addEventListener('resize', scheduleVisualViewportUpdate);
|
||||
window.addEventListener('orientationchange', scheduleVisualViewportUpdate);
|
||||
const isTextInputTarget = (element: HTMLElement | null) => {
|
||||
if (!element) {
|
||||
return false;
|
||||
@@ -504,7 +516,7 @@ export const MainLayout: React.FC = () => {
|
||||
if (isTextInputTarget(target)) {
|
||||
ignoreOpenUntilZero = false;
|
||||
}
|
||||
updateVisualViewport();
|
||||
scheduleVisualViewportUpdate();
|
||||
};
|
||||
document.addEventListener('focusin', handleFocusIn, true);
|
||||
|
||||
@@ -549,10 +561,11 @@ export const MainLayout: React.FC = () => {
|
||||
document.addEventListener('focusout', handleFocusOut, true);
|
||||
|
||||
return () => {
|
||||
viewport?.removeEventListener('resize', updateVisualViewport);
|
||||
viewport?.removeEventListener('scroll', updateVisualViewport);
|
||||
window.removeEventListener('resize', updateVisualViewport);
|
||||
window.removeEventListener('orientationchange', updateVisualViewport);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
viewport?.removeEventListener('resize', scheduleVisualViewportUpdate);
|
||||
viewport?.removeEventListener('scroll', scheduleVisualViewportUpdate);
|
||||
window.removeEventListener('resize', scheduleVisualViewportUpdate);
|
||||
window.removeEventListener('orientationchange', scheduleVisualViewportUpdate);
|
||||
document.removeEventListener('focusin', handleFocusIn, true);
|
||||
document.removeEventListener('focusout', handleFocusOut, true);
|
||||
clearKeyboardAvoidTarget();
|
||||
|
||||
@@ -161,12 +161,19 @@ export function useDeviceInfo(): DeviceInfo {
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout>;
|
||||
const handleResize = () => {
|
||||
setDeviceInfo(getDeviceInfo());
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
setDeviceInfo(getDeviceInfo());
|
||||
}, 150);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
return () => {
|
||||
clearTimeout(debounceTimer);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user