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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user