perf(app): keep warm workspace switching responsive (#3447)

* perf(app): reduce warm workspace switch work

Retained workspaces avoided mounts but pane activity still rerendered the full composer, message input, and agent-control presentation. Keep the activity contract intact while moving focus-sensitive behavior behind small context consumers, and add a Playwright benchmark that measures rapid retained workspace switching on one browser clock.

* perf(app): accelerate retained workspace switching

Avoid repeated retained-stream layout reconciliation and unrelated workspace structure recomputation. Use the lightweight route transition while the host is already focused, while preserving targeted root POP_TO for returns from app-wide routes where it prevents duplicate hidden decks.

* perf(app): defer composer focus during workspace switches

* perf(app): retain recently used workspaces longer

Keep up to ten workspace screens warm so normal multi-workspace navigation does not repeatedly rebuild recently used timelines. Inactive screens expire after ten minutes, while the active screen remains protected and capacity eviction stays least-recently-used.

* fix(app): reconcile retained timeline geometry

* fix(app): refresh retained reading position
This commit is contained in:
Mohamed Boudra
2026-08-16 12:40:28 +02:00
committed by GitHub
parent d60d92e909
commit d4435bd016
20 changed files with 1297 additions and 228 deletions
+112 -4
View File
@@ -202,6 +202,65 @@ function syncNearBottom(
return nextValue;
}
interface ActiveFollowOutputLayout {
scrollContainer: HTMLElement | null;
viewportWidth: number;
viewportHeight: number;
activationKey: string;
isActivationReady: boolean;
renderLiveAuxiliary: StreamRenderInput["renderers"]["renderLiveAuxiliary"];
historyMounted: StreamRenderInput["segments"]["historyMounted"];
historyVirtualized: StreamRenderInput["segments"]["historyVirtualized"];
liveHead: StreamRenderInput["segments"]["liveHead"];
virtualTotalSize: number;
}
function activeFollowOutputLayoutsEqual(
previous: ActiveFollowOutputLayout,
next: ActiveFollowOutputLayout,
): boolean {
return (
previous.scrollContainer === next.scrollContainer &&
previous.viewportWidth === next.viewportWidth &&
previous.viewportHeight === next.viewportHeight &&
previous.activationKey === next.activationKey &&
previous.isActivationReady === next.isActivationReady &&
previous.renderLiveAuxiliary === next.renderLiveAuxiliary &&
previous.historyMounted === next.historyMounted &&
previous.historyVirtualized === next.historyVirtualized &&
previous.liveHead === next.liveHead &&
previous.virtualTotalSize === next.virtualTotalSize
);
}
interface ObservedViewportGeometry {
clientWidth: number;
clientHeight: number;
scrollWidth: number;
scrollHeight: number;
}
function getObservedViewportGeometry(scrollContainer: HTMLElement): ObservedViewportGeometry {
return {
clientWidth: scrollContainer.clientWidth,
clientHeight: scrollContainer.clientHeight,
scrollWidth: scrollContainer.scrollWidth,
scrollHeight: scrollContainer.scrollHeight,
};
}
function observedViewportGeometriesEqual(
previous: ObservedViewportGeometry,
next: ObservedViewportGeometry,
): boolean {
return (
previous.clientWidth === next.clientWidth &&
previous.clientHeight === next.clientHeight &&
previous.scrollWidth === next.scrollWidth &&
previous.scrollHeight === next.scrollHeight
);
}
function getScrollContainerDistanceFromBottom(
scrollContainer: Pick<HTMLElement, "scrollTop" | "clientHeight" | "scrollHeight">,
): number {
@@ -275,6 +334,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
const historyStartPrependAnchorRef = useRef<HistoryStartPrependAnchor | null>(null);
const historyStartPrependAnchorActiveRef = useRef(false);
const historyStartSettleSchedulerRef = useRef<HistoryStartSettleScheduler | null>(null);
const lastActiveFollowOutputLayoutRef = useRef<ActiveFollowOutputLayout | null>(null);
const lastObservedViewportGeometryRef = useRef<ObservedViewportGeometry | null>(null);
const wasFollowOutputLayoutActiveRef = useRef(false);
const resumedUnchangedLayoutRef = useRef(false);
const pendingResumeGeometryCheckRef = useRef(false);
const shouldUseVirtualizer = segments.historyVirtualized.length > 0;
const {
renderHistoryVirtualizedRow,
@@ -753,14 +817,39 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
// Following output is a layout invariant: rows, footer, and bottom offset must
// reach the browser in the same paint.
useLayoutEffect(() => {
if (!isActive || !followOutputRef.current) {
const layout: ActiveFollowOutputLayout = {
scrollContainer: scrollContainerRef.current,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
activationKey,
isActivationReady,
renderLiveAuxiliary,
historyMounted: segments.historyMounted,
historyVirtualized: segments.historyVirtualized,
liveHead: segments.liveHead,
virtualTotalSize,
};
const resumedUnchangedLayout = Boolean(
isActive &&
!wasFollowOutputLayoutActiveRef.current &&
lastActiveFollowOutputLayoutRef.current &&
activeFollowOutputLayoutsEqual(lastActiveFollowOutputLayoutRef.current, layout),
);
resumedUnchangedLayoutRef.current = resumedUnchangedLayout;
pendingResumeGeometryCheckRef.current = resumedUnchangedLayout;
wasFollowOutputLayoutActiveRef.current = isActive;
if (!isActive) {
return;
}
lastActiveFollowOutputLayoutRef.current = layout;
if (!followOutputRef.current || resumedUnchangedLayout) return;
cancelPendingStickToBottom();
scrollMessagesToBottom("auto");
}, [
activationKey,
cancelPendingStickToBottom,
isActive,
isActivationReady,
renderLiveAuxiliary,
scrollMessagesToBottom,
segments.historyMounted,
@@ -773,7 +862,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
if (!isActive) {
return;
}
updateScrollMetrics();
const resumedUnchangedLayout = resumedUnchangedLayoutRef.current;
resumedUnchangedLayoutRef.current = false;
if (!resumedUnchangedLayout) {
updateScrollMetrics();
}
evaluateHistoryStart();
if (historyStartPaginationStateRef.current.status === "settling") {
scheduleHistoryStartPrependSettle();
@@ -802,9 +895,23 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
return;
}
updateScrollMetrics();
evaluateHistoryStart();
if (!pendingResumeGeometryCheckRef.current) {
updateScrollMetrics();
evaluateHistoryStart();
}
const observer = new ResizeObserver(() => {
const nextGeometry = getObservedViewportGeometry(scrollContainer);
if (pendingResumeGeometryCheckRef.current) {
pendingResumeGeometryCheckRef.current = false;
reportReadingPosition();
const previousGeometry = lastObservedViewportGeometryRef.current;
lastObservedViewportGeometryRef.current = nextGeometry;
if (previousGeometry && observedViewportGeometriesEqual(previousGeometry, nextGeometry)) {
return;
}
} else {
lastObservedViewportGeometryRef.current = nextGeometry;
}
if (historyStartPrependAnchorActiveRef.current) {
applyHistoryStartPrependAnchor();
}
@@ -829,6 +936,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
applyHistoryStartPrependAnchor,
evaluateHistoryStart,
isActive,
reportReadingPosition,
scheduleHistoryStartPrependSettle,
scheduleStickToBottom,
updateScrollMetrics,