From 7a62ec049c654610ad1648f36f8a625841eef32e Mon Sep 17 00:00:00 2001 From: Badcuban <108198679+badcuban@users.noreply.github.com> Date: Sat, 14 Nov 2026 12:02:37 -0500 Subject: [PATCH] perf(web): the browser panel's host loads in the desktop app only The layer that holds every thread's browser pages, and this window's browser connection for agents, was imported by the app's root, so a browser tab and a phone downloaded and parsed about 70 KB of code they can never use: the web build has no . It is now a chunk the desktop app fetches as it starts. Startup code drops from 913,440 B to 890,863 B gzip (the entry chunk from 249,661 B to 227,551 B), which also gives the startup budget back its headroom. A desktop-window browser test checks the window still mounts the layer. --- .../ChatView.desktop-window.browser.tsx | 21 +++++++++++++++++++ apps/web/src/routes/__root.tsx | 21 ++++++++++++++++--- scripts/check-web-bundle-size.ts | 8 ++++--- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ChatView.desktop-window.browser.tsx b/apps/web/src/components/ChatView.desktop-window.browser.tsx index b1af382a7..6f38db70d 100644 --- a/apps/web/src/components/ChatView.desktop-window.browser.tsx +++ b/apps/web/src/components/ChatView.desktop-window.browser.tsx @@ -94,4 +94,25 @@ describe("ChatView (full app): the desktop app's window", () => { root.classList.remove("electron", "wco"); } }); + + it("mounts the layer that holds every thread's browser pages", async () => { + // Its code is a chunk only the desktop app fetches (routes/__root.tsx), so + // the window has to load it and mount it by itself, with no panel open. + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-desktop-browser-host" as MessageId, + targetText: "desktop browser host", + }), + }); + + try { + await waitForElement( + () => document.querySelector('[data-testid="browser-host-layer"]'), + "The desktop app should mount the browser host layer at its root.", + ); + } finally { + await mounted.cleanup(); + } + }); }); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 6c4f13b5d..2e6b8f566 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -16,7 +16,7 @@ import { useNavigate, useRouter, } from "@tanstack/react-router"; -import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react"; +import { Suspense, useCallback, useEffect, useEffectEvent, useRef, useState } from "react"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import { APP_DISPLAY_NAME } from "../branding"; @@ -24,7 +24,6 @@ import { AppSidebarLayout } from "../components/AppSidebarLayout"; import { CommandPalette, preloadCommandPaletteDialog } from "../components/CommandPalette"; import { preloadDestinationLists } from "../components/sidebar/destinationLists"; import { PushAlertsRuntime } from "../pushAlerts/PushAlertsRuntime"; -import { BrowserHostLayer } from "../components/browser/BrowserHostLayer"; import { BrowserProfileReconciler } from "../components/desktop/BrowserProfileReconciler"; import { DesktopUpdateInstallDialog } from "../components/desktop/DesktopUpdateInstallDialog"; import { SshPasswordPromptDialog } from "../components/desktop/SshPasswordPromptDialog"; @@ -163,6 +162,17 @@ import { type StaleBuildRecovery, } from "../staleBuild"; +// Every thread's browser pages and this window's browser connection for +// agents: the desktop app only, since the web build has no ``. A +// chunk of its own, so a browser tab or a phone never downloads it; the +// desktop app fetches it as it starts, beside everything else. +const BrowserHostLayerLazy = lazyWithPreload(() => + import("../components/browser/BrowserHostLayer").then((module) => module.BrowserHostLayer), +); +if (isElectron) { + void BrowserHostLayerLazy.preload(); +} + // The theme library's sync across computers: after first paint, never at startup. const ThemeLibrarySyncLazy = lazyWithPreload(() => import("../themeLibrary/themeLibraryState").then((module) => module.ThemeLibrarySync), @@ -276,7 +286,12 @@ function RootRouteView() { {/* The Keep bar every theme trial shows, the import dialog, theme file drops. */} {isElectron ? : null} - {isElectron ? : null} + {/* Plain Suspense: the browser panel needs it, so a failure shows. */} + {isElectron ? ( + + + + ) : null} diff --git a/scripts/check-web-bundle-size.ts b/scripts/check-web-bundle-size.ts index 5431208db..aa98a97ec 100644 --- a/scripts/check-web-bundle-size.ts +++ b/scripts/check-web-bundle-size.ts @@ -51,9 +51,11 @@ const DEFAULT_ENTRY_GZIP_BUDGET_BYTES = 255_000; // Raised to 915 KB (2026-11-13, Will's OK): main sat 49 B under the budget // (911,951 B in CI), so the browser's search engine setting (#907), about // 200 B of Settings catalog and schema once its address rules and search URLs -// were moved behind the browser panel, read 912,165 B. Worth trimming next: -// the browser panel's host and automation load at startup on the web and -// phones too, which have no browser panel. +// were moved behind the browser panel, read 912,165 B. +// Measured 890,863 B here (2026-11-14) once the browser panel's host and +// automation, which only the desktop app has a use for, became a chunk only +// it fetches (routes/__root.tsx): about 22 KB back. The budget stays, as +// headroom. const DEFAULT_STARTUP_GZIP_BUDGET_BYTES = 915_000; const entryBudgetBytes = Number(