fix(app): avoid pane rerenders while resizing Explorer (#7284)

Keep drag widths out of the shared stylesheet and skip unchanged panes during width previews. Preserve the existing resize state and persistence behavior.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Malison <1246619+colonelpanic8@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-18 09:25:57 +02:00
committed by GitHub
co-authored by Ivan Malison
parent a65478d0bf
commit 095dd0316e
2 changed files with 125 additions and 4 deletions
@@ -0,0 +1,118 @@
import type { Page } from "@playwright/test";
import { test, expect } from "../support/fixtures";
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
interface DragMeasurement {
observer: MutationObserver;
stylesheetMutations: number;
}
declare global {
interface Window {
__PASEO_EXPLORER_DRAG_MEASUREMENT__?: DragMeasurement;
}
}
async function waitForResizePaint(page: Page): Promise<void> {
await page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
}),
);
}
async function beginResizeMeasurement(page: Page): Promise<void> {
await page.evaluate(() => {
const stylesheet = document.getElementById("unistyles-web");
if (!stylesheet) throw new Error("Shared stylesheet is missing");
globalThis.__PASEO_RESET_RENDER_PROFILE__!();
const measurement: DragMeasurement = {
stylesheetMutations: 0,
observer: new MutationObserver((records) => {
measurement.stylesheetMutations += records.length;
}),
};
measurement.observer.observe(stylesheet, {
childList: true,
subtree: true,
characterData: true,
});
window.__PASEO_EXPLORER_DRAG_MEASUREMENT__ = measurement;
});
}
async function previewExplorerResize(page: Page, distance: number) {
const handle = page
.getByTestId("workspace-explorer-sidebar-resize-handle")
.getByRole("separator");
const explorer = page.getByTestId("workspace-explorer-sidebar");
const bounds = await handle.boundingBox();
if (!bounds) throw new Error("Explorer resize handle is missing");
const before = (await explorer.boundingBox())!.width;
const x = bounds.x + bounds.width / 2;
const y = bounds.y + 100;
await page.mouse.move(x, y);
await page.mouse.down();
await waitForResizePaint(page);
await beginResizeMeasurement(page);
for (let move = 1; move <= 20; move++) {
await page.mouse.move(x + (distance * move) / 20, y);
await waitForResizePaint(page);
}
const during = (await explorer.boundingBox())!.width;
const measurement = await page.evaluate(() => {
const state = window.__PASEO_EXPLORER_DRAG_MEASUREMENT__!;
state.observer.disconnect();
return {
stylesheetMutations: state.stylesheetMutations,
paneRenders: Object.entries(globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {})
.filter(([id]) => id.startsWith("SplitPaneView:"))
.reduce((sum, [, reasons]) => sum + (reasons.render ?? 0), 0),
};
});
await page.mouse.up();
await waitForResizePaint(page);
const after = (await explorer.boundingBox())!.width;
expect(during).toBeCloseTo(before - distance, 0);
expect(after).toBeCloseTo(during, 0);
return { ...measurement, width: after };
}
async function expectExplorerWidthSurvivesReload(page: Page, width: number): Promise<void> {
await page.reload();
const explorer = page.getByTestId("workspace-explorer-sidebar");
await expect(explorer).toBeVisible();
await expect.poll(async () => (await explorer.boundingBox())?.width).toBeCloseTo(width, 0);
}
test("Explorer drag previews without rerendering panes or rewriting the shared stylesheet", async ({
page,
withWorkspace,
}) => {
await page.setViewportSize({ width: 1600, height: 1000 });
await page.addInitScript(() => {
globalThis.__PASEO_RENDER_PROFILE_ENABLED__ = true;
});
const workspace = await withWorkspace();
await workspace.navigateTo();
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
await page.getByTestId("workspace-explorer-toggle").first().click();
await expect(page.getByTestId("workspace-explorer-sidebar")).toBeVisible();
// Verify that direct pane renders are being counted before resetting them.
expect(
await page.evaluate(() =>
Object.keys(globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {}).some((id) =>
id.startsWith("SplitPaneView:"),
),
),
).toBe(true);
const grown = await previewExplorerResize(page, -100);
const shrunk = await previewExplorerResize(page, 50);
expect(grown.stylesheetMutations).toBe(0);
expect(grown.paneRenders).toBe(0);
expect(shrunk.stylesheetMutations).toBe(0);
expect(shrunk.paneRenders).toBe(0);
await expectExplorerWidthSurvivesReload(page, shrunk.width);
});
@@ -1,5 +1,6 @@
import {
Fragment,
memo,
useCallback,
useEffect,
useMemo,
@@ -88,7 +89,8 @@ import {
type WorkspaceLayout,
} from "@/stores/workspace-layout-store";
import type { WorkspaceTab } from "@/workspace-tabs/model";
import { RenderProfile } from "@/utils/render-profiler";
import { recordRenderProfileReasons, RenderProfile } from "@/utils/render-profiler";
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
import { isNative } from "@/constants/platform";
import { panelTargetSupportsHost } from "@/plugins/workspace-panels/locations";
@@ -435,7 +437,7 @@ export function SplitContainer({
: inheritedWindowChromeCorners;
const mainColumnStyle = styles.mainColumn;
const explorerSidebarDockStyle = useMemo(
() => [styles.explorerSidebarDock, { width: explorerSidebarWidth }],
() => [styles.explorerSidebarDock, inlineUnistylesStyle({ width: explorerSidebarWidth })],
[explorerSidebarWidth],
);
const handleWorkspaceShellLayout = useCallback((event: LayoutChangeEvent) => {
@@ -1121,7 +1123,7 @@ function SplitNodeView({
);
}
function SplitPaneView({
const SplitPaneView = memo(function SplitPaneView({
pane,
uiTabs,
isFocused,
@@ -1158,6 +1160,7 @@ function SplitPaneView({
focusModeEnabled,
onExitFocusMode,
}: SplitPaneViewProps) {
recordRenderProfileReasons(`SplitPaneView:${pane.id}`, ["render"]);
const paneRef = useRef<View | null>(null);
const stableOnFocusPane = useStableEvent(onFocusPane);
const paneState = useMemo(
@@ -1312,7 +1315,7 @@ function SplitPaneView({
</View>
</RenderProfile>
);
}
});
function collectPanesById(node: SplitNode): Map<string, SplitPane> {
const next = new Map<string, SplitPane>();