diff --git a/apps/web/src/browser/ServerBrowserSurface.tsx b/apps/web/src/browser/ServerBrowserSurface.tsx index c5944e2d020d955a77f199793bac134c09934f6b..b8f403598d0c19a9883de2bd07cc1dce6611c7bc 100644 --- a/apps/web/src/browser/ServerBrowserSurface.tsx +++ b/apps/web/src/browser/ServerBrowserSurface.tsx @@ -52,7 +52,7 @@ export interface ServerBrowserHandle { readonly canvas: () => HTMLCanvasElement | null; } -const RESIZE_DEBOUNCE_MS = 150; +const RESIZE_THROTTLE_MS = 150; const ACCESS_RETRY_MS = 10_000; // A recent probe answer near a new tap stands in for that tap's own answer, // which on a slow link arrives after the tap ends. @@ -334,15 +334,11 @@ export function ServerBrowserSurface(props: { const element = canvasRef.current?.parentElement; if (!element) return; let timer: ReturnType | null = null; - const measure = () => { - timer = null; - const rect = element.getBoundingClientRect(); - if (rect.width < 1 || rect.height < 1) return; - const size = { width: Math.round(rect.width), height: Math.round(rect.height) }; - const previous = sizeRef.current; - if (previous?.width === size.width && previous.height === size.height) return; - sizeRef.current = size; - if (followSize) clientRef.current?.send({ type: "resize", ...size }); + let capTimer: ReturnType | null = null; + const growCap = () => { + capTimer = null; + const size = sizeRef.current; + if (!size) return; const ratio = window.devicePixelRatio || 1; const width = Math.round(size.width * ratio); const height = Math.round(size.height * ratio); @@ -355,14 +351,31 @@ export function ServerBrowserSurface(props: { }, ); }; + const measure = () => { + timer = null; + const rect = element.getBoundingClientRect(); + if (rect.width < 1 || rect.height < 1) return; + const size = { width: Math.round(rect.width), height: Math.round(rect.height) }; + const previous = sizeRef.current; + if (previous?.width === size.width && previous.height === size.height) return; + sizeRef.current = size; + if (followSize) clientRef.current?.send({ type: "resize", ...size }); + // Outgrowing the cap reconnects the stream, so grow it once the size settles + // rather than on every throttled resize of a drag. + if (capTimer !== null) clearTimeout(capTimer); + capTimer = setTimeout(growCap, previous === null ? 0 : RESIZE_THROTTLE_MS * 2); + }; const stopObserving = observeResize(element, () => { - if (timer !== null) clearTimeout(timer); - // The first size connects right away; later ones settle before resizing the page. - timer = setTimeout(measure, sizeRef.current === null ? 0 : RESIZE_DEBOUNCE_MS); + // A pending measure reads the latest size when it fires, so a drag resizes + // the page at most every RESIZE_THROTTLE_MS and still ends on the final size. + if (timer !== null) return; + // The first size connects right away. + timer = setTimeout(measure, sizeRef.current === null ? 0 : RESIZE_THROTTLE_MS); }); return () => { stopObserving(); if (timer !== null) clearTimeout(timer); + if (capTimer !== null) clearTimeout(capTimer); }; }, [followSize]);