From 52130627edf8b34892eaf8ac37670f0a43343472 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Sun, 28 Jun 2026 11:09:23 -0700 Subject: [PATCH 1/5] Improve web app startup resilience on slow links --- apps/app/src/App.tsx | 18 +- .../DiffWorkerPoolProvider.tsx | 27 +++ .../src/views/RootComposeSecondaryContent.tsx | 98 ++++++--- apps/app/src/views/RootComposeView.tsx | 189 ++++++++++-------- .../views/thread-detail/ThreadDetailRoute.tsx | 21 +- apps/server/src/server.ts | 2 + apps/server/test/app/static-cache.test.ts | 11 +- scripts/measure-app-startup-payload.mjs | 120 +++++++++++ 8 files changed, 351 insertions(+), 135 deletions(-) create mode 100644 apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx create mode 100644 scripts/measure-app-startup-payload.mjs diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 858671fb1..f6ed82396 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useEffect } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { AppLayout } from "./components/layout/AppLayout"; +import { PageShell } from "./components/ui/page-shell"; import { AuthCallbackView } from "./views/AuthCallbackView"; import { RootComposeRoute } from "./views/RootComposeView"; import { QuickCreateProjectProvider } from "./hooks/useQuickCreateProject"; @@ -98,14 +99,11 @@ function PopoutRouteFallback() { function AppRoutes() { return ( - + }> } /> } /> - } - /> + } /> } @@ -144,6 +142,16 @@ function AppRoutes() { ); } +function AppRouteFallback() { + return ( + +

+ Loading... +

+
+ ); +} + export function App() { // Connect WebSocket for real-time invalidation useWebSocket(); diff --git a/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx b/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx new file mode 100644 index 000000000..3f3eb0b52 --- /dev/null +++ b/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; +import { WorkerPoolContextProvider } from "@pierre/diffs/react"; +import { + createDiffWorker, + getDiffWorkerPoolSize, +} from "@/lib/diff-worker-pool"; + +const WORKER_POOL_OPTIONS = { + workerFactory: createDiffWorker, + poolSize: getDiffWorkerPoolSize(), +}; +const HIGHLIGHTER_OPTIONS = {}; + +export default function DiffWorkerPoolProvider({ + children, +}: { + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/app/src/views/RootComposeSecondaryContent.tsx b/apps/app/src/views/RootComposeSecondaryContent.tsx index 0ba705f62..40b0394a2 100644 --- a/apps/app/src/views/RootComposeSecondaryContent.tsx +++ b/apps/app/src/views/RootComposeSecondaryContent.tsx @@ -1,11 +1,12 @@ import { + lazy, + Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, - type ComponentProps, type ReactNode, } from "react"; import { useAtomValue } from "jotai"; @@ -16,7 +17,7 @@ import { } from "react-resizable-panels"; import { ResponsiveDrawerShell } from "@/components/ui/responsive-overlay.js"; import { useIsCompactViewport } from "@/components/ui/hooks/use-compact-viewport.js"; -import { ThreadSecondaryPanel } from "@/components/secondary-panel/ThreadSecondaryPanel"; +import type { ThreadSecondaryPanelProps } from "@/components/secondary-panel/ThreadSecondaryPanel"; import { secondaryPanelWidthPercentAtom } from "@/components/secondary-panel/threadSecondaryPanelAtoms"; import { PANEL_COLLAPSE_TRANSITION_CLASS } from "@/components/secondary-panel/panelTransitionTokens"; import { PAGE_SHELL_CONTENT_STYLE } from "@/components/ui/page-shell-content-style.js"; @@ -26,9 +27,17 @@ import { cn } from "@/lib/utils"; const CLOSED_MAIN_PANEL_SIZE_PERCENT = 100; const MAIN_PANEL_MIN_SIZE_PERCENT = 30; const ROOT_COMPOSE_MAX_WIDTH_CLASS = "max-w-[760px]"; +const LazyThreadSecondaryPanel = lazy(() => + import("@/components/secondary-panel/ThreadSecondaryPanel").then( + (module) => ({ default: module.ThreadSecondaryPanel }), + ), +); +const LazyDiffWorkerPoolProvider = lazy( + () => import("@/components/secondary-panel/DiffWorkerPoolProvider"), +); type RootSecondaryPanelProps = Omit< - ComponentProps, + ThreadSecondaryPanelProps, | "browserDeck" | "isConversationCollapsed" | "onToggleConversationCollapse" @@ -49,6 +58,28 @@ interface RootComposeSecondaryContentProps { function noopToggleConversationCollapse(): void {} +function RootComposeDiffWorkerPoolBoundary({ + children, + fallback, +}: { + children: ReactNode; + fallback: ReactNode; +}) { + return ( + + {children} + + ); +} + +function RootComposeSecondaryPanelLoading() { + return ( +
+ Loading... +
+ ); +} + export function RootComposeSecondaryContent({ children, contentClassName, @@ -176,26 +207,47 @@ export function RootComposeSecondaryContent({ secondaryWidth, ]); }, [isSecondaryPanelOpen, renderAsDrawer]); - const inlineSecondaryPanelContent = !renderAsDrawer ? ( - - ) : null; - const drawerSecondaryPanelContent = renderAsDrawer ? ( - - ) : null; + const inlineSecondaryPanelFallback = ( + + + + ); + const inlineSecondaryPanelContent = + !renderAsDrawer && isSecondaryPanelOpen ? ( + + + + ) : null; + const drawerSecondaryPanelContent = + renderAsDrawer && isSecondaryPanelOpen ? ( + } + > + + + ) : null; return (
diff --git a/apps/app/src/views/RootComposeView.tsx b/apps/app/src/views/RootComposeView.tsx index fb5a1e059..02d5c4dc4 100644 --- a/apps/app/src/views/RootComposeView.tsx +++ b/apps/app/src/views/RootComposeView.tsx @@ -1,4 +1,6 @@ import { + lazy, + Suspense, useCallback, useEffect, useMemo, @@ -7,7 +9,6 @@ import { type ReactNode, } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; -import { WorkerPoolContextProvider } from "@pierre/diffs/react"; import { findLocalPathProjectSourceForHost, type EnvironmentStatus, @@ -48,13 +49,6 @@ import type { ProjectSelectorOption } from "@/components/pickers/ProjectSelector import type { ReuseThreadOption } from "@/components/pickers/WorktreePicker"; import { HEADER_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; import type { SecondaryPanelFileTab } from "@/components/secondary-panel/ThreadSecondaryPanel"; -import { FilePreview } from "@/components/secondary-panel/FilePreview"; -import { - HostFilePreviewTabContent, - ProjectFilePreviewTabContent, - ThreadStorageFilePreviewTabContent, - WorkspaceFilePreviewTabContent, -} from "@/components/secondary-panel/ThreadSecondaryPanelTabContent"; import { BrowserTabDeck } from "@/components/secondary-panel/BrowserTabDeck"; import { NewTabPage } from "@/components/secondary-panel/NewTabPage"; import { EmptyStatePanel } from "@/components/ui/empty-state"; @@ -81,9 +75,7 @@ import { } from "@/hooks/queries/project-queries"; import { useEnvironment } from "@/hooks/queries/environment-queries"; import { useProjectDefaultExecutionOptions } from "@/hooks/queries/project-default-execution-options-query"; -import { - useHostProviderCliStatus, -} from "@/hooks/queries/system-queries"; +import { useHostProviderCliStatus } from "@/hooks/queries/system-queries"; import { useSidebarNavigation } from "@/hooks/queries/sidebar-navigation-query"; import { useThreads } from "@/hooks/queries/thread-queries"; import { useCommandSuggestions } from "@/hooks/useCommandSuggestions"; @@ -170,7 +162,6 @@ import { type FileSearchSelection, } from "@/components/secondary-panel/useThreadFileTabs"; import { resolveRightPanelFileVisual } from "@/components/secondary-panel/rightPanelFileVisuals"; -import { ThreadTerminalPanel } from "@/components/thread/terminal/ThreadTerminalPanel"; import { DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS, @@ -193,10 +184,6 @@ import { resolveEnvironmentOpenContext, resolveThreadWorkspacePreviewRootPath, } from "./thread-detail/threadWorkspaceOpenPath"; -import { - createDiffWorker, - getDiffWorkerPoolSize, -} from "@/lib/diff-worker-pool"; const ROOT_COMPOSE_ZEN_MODE_STORAGE_KEY = "bb.promptbox.zen-mode.root-compose"; const ROOT_COMPOSE_SIDEBAR_ACTION_ALIGNED_TOP_PADDING_CLASS = "pt-14"; @@ -223,11 +210,36 @@ const ROOT_COMPOSE_EMPTY_WELCOME_CONTENT_CLASS = "min-h-full flex-1 items-center justify-center pb-12"; const ROOT_COMPOSE_FIXED_PANEL_STATE_ID = "root-compose"; const EMPTY_TERMINAL_SESSIONS: readonly TerminalSession[] = []; -const FILE_PREVIEW_WORKER_POOL_OPTIONS = { - workerFactory: createDiffWorker, - poolSize: getDiffWorkerPoolSize(), -}; -const FILE_PREVIEW_HIGHLIGHTER_OPTIONS = {}; +const LazyFilePreview = lazy(() => + import("@/components/secondary-panel/FilePreview").then((module) => ({ + default: module.FilePreview, + })), +); +const LazyWorkspaceFilePreviewTabContent = lazy(() => + import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( + (module) => ({ default: module.WorkspaceFilePreviewTabContent }), + ), +); +const LazyProjectFilePreviewTabContent = lazy(() => + import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( + (module) => ({ default: module.ProjectFilePreviewTabContent }), + ), +); +const LazyHostFilePreviewTabContent = lazy(() => + import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( + (module) => ({ default: module.HostFilePreviewTabContent }), + ), +); +const LazyThreadStorageFilePreviewTabContent = lazy(() => + import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( + (module) => ({ default: module.ThreadStorageFilePreviewTabContent }), + ), +); +const LazyThreadTerminalPanel = lazy(() => + import("@/components/thread/terminal/ThreadTerminalPanel").then((module) => ({ + default: module.ThreadTerminalPanel, + })), +); type ProjectSelectionChangeHandler = NewThreadProjectConfig["onChange"]; type SecondaryPanelChangeHandler = (panel: ThreadSecondaryPanelTab) => void; @@ -728,7 +740,9 @@ export function buildRootComposeTerminalSessions({ environmentTerminalSessions, globalTerminalSessions, terminalTarget, -}: BuildRootComposeTerminalSessionsArgs): readonly TerminalSession[] | undefined { +}: BuildRootComposeTerminalSessionsArgs): + | readonly TerminalSession[] + | undefined { if (terminalTarget?.kind === "environment") { return environmentTerminalSessions; } @@ -819,6 +833,14 @@ function CodexCliVersionBanner({ ); } +function RootComposeFileTabLoading() { + return ( + + Loading... + + ); +} + export function RootComposeRoute() { const { projectId } = useParams<{ projectId: string }>(); @@ -826,14 +848,7 @@ export function RootComposeRoute() { return ; } - return ( - - - - ); + return ; } export function RootComposeView(props: RootComposeViewProps) { @@ -898,7 +913,9 @@ export function RootComposeView(props: RootComposeViewProps) { const primaryHost = useMemo(() => { const hosts = hostsQuery.data; if (!hosts || hosts.length === 0) return null; - return hosts.find((host) => host.status === "connected") ?? hosts[0] ?? null; + return ( + hosts.find((host) => host.status === "connected") ?? hosts[0] ?? null + ); }, [hostsQuery.data]); const primaryHostId = primaryHost?.id ?? null; const uploadPromptAttachment = useUploadPromptAttachment(); @@ -1832,21 +1849,20 @@ export function RootComposeView(props: RootComposeViewProps) { const shouldUseRootStorageViewerForActiveTab = rawActiveRootStorageFileThreadId !== null && rawActiveRootStorageFileThreadId === rootPanelThreadId; - const { - threadStorageRootPath: activeStorageThreadStorageRootPath, - } = useThreadStorageViewer({ - activePath: null, - fileListEnabled: - props.surface === "page" && - rawActiveRootStorageFileThreadId !== null && - !shouldUseRootStorageViewerForActiveTab, - filePreviewEnabled: false, - threadId: - rawActiveRootStorageFileThreadId !== null && - !shouldUseRootStorageViewerForActiveTab - ? rawActiveRootStorageFileThreadId - : undefined, - }); + const { threadStorageRootPath: activeStorageThreadStorageRootPath } = + useThreadStorageViewer({ + activePath: null, + fileListEnabled: + props.surface === "page" && + rawActiveRootStorageFileThreadId !== null && + !shouldUseRootStorageViewerForActiveTab, + filePreviewEnabled: false, + threadId: + rawActiveRootStorageFileThreadId !== null && + !shouldUseRootStorageViewerForActiveTab + ? rawActiveRootStorageFileThreadId + : undefined, + }); const activeStorageFileRootPath = shouldUseRootStorageViewerForActiveTab ? rootThreadStorageRootPath : activeStorageThreadStorageRootPath; @@ -1877,7 +1893,8 @@ export function RootComposeView(props: RootComposeViewProps) { const loadedTerminalSessions = useMemo( () => buildRootComposeTerminalSessions({ - environmentTerminalSessions: environmentTerminalsListQuery.data?.sessions, + environmentTerminalSessions: + environmentTerminalsListQuery.data?.sessions, globalTerminalSessions: globalTerminalsListQuery.data?.sessions, terminalTarget: rootPanelTerminalTarget, }), @@ -1887,8 +1904,7 @@ export function RootComposeView(props: RootComposeViewProps) { rootPanelTerminalTarget, ], ); - const terminalSessions = - loadedTerminalSessions ?? EMPTY_TERMINAL_SESSIONS; + const terminalSessions = loadedTerminalSessions ?? EMPTY_TERMINAL_SESSIONS; const terminalsListLoaded = loadedTerminalSessions !== undefined; const activeTerminalCount = useMemo( () => @@ -2007,13 +2023,12 @@ export function RootComposeView(props: RootComposeViewProps) { const closeRootSecondaryPanel = useCallback(() => { setRootSecondaryPanelForSurface(null); }, [setRootSecondaryPanelForSurface]); - const openRootSecondaryPanel = - useCallback( - (panel) => { - setRootSecondaryPanelForSurface(panel); - }, - [setRootSecondaryPanelForSurface], - ); + const openRootSecondaryPanel = useCallback( + (panel) => { + setRootSecondaryPanelForSurface(panel); + }, + [setRootSecondaryPanelForSurface], + ); const toggleRootPersistedSecondaryPanel = useCallback(() => { if (isPersistedSecondaryPanelOpen) { closeRootSecondaryPanel(); @@ -2183,11 +2198,7 @@ export function RootComposeView(props: RootComposeViewProps) { }); }, [browserTabIds, openBrowserTabAndReveal]); const renderBrowserDeck = useCallback( - ({ - canShowNativeBrowserView, - }: { - canShowNativeBrowserView: boolean; - }) => { + ({ canShowNativeBrowserView }: { canShowNativeBrowserView: boolean }) => { if (rootPanelThreadId === null) { return null; } @@ -2236,14 +2247,13 @@ export function RootComposeView(props: RootComposeViewProps) { } handleOpenNewTab(); }, [closeSecondaryPanel, handleOpenNewTab, isSecondaryPanelOpen]); - const handleSecondaryPanelChange = - useCallback( - (panel) => { - clearActiveFileTabs(); - openSecondaryPanel(panel); - }, - [clearActiveFileTabs, openSecondaryPanel], - ); + const handleSecondaryPanelChange = useCallback( + (panel) => { + clearActiveFileTabs(); + openSecondaryPanel(panel); + }, + [clearActiveFileTabs, openSecondaryPanel], + ); const handleSecondaryPanelFocus = useCallback(() => { touchFixedPanelTabsState(); }, [touchFixedPanelTabsState]); @@ -2654,9 +2664,9 @@ export function RootComposeView(props: RootComposeViewProps) { activeTabId: activeFixedSecondaryTabId, tabs: syncedOrderedSecondaryFileTabs, }); - const fileTabContent: ReactNode = + const fileTabContentBody: ReactNode = activeTerminalId && rootPanelTerminalTarget ? ( - ) : activeWorkspaceFilePath !== null && activeWorkspaceFileEnvironmentId !== null ? ( - ) : activeWorkspaceFilePath !== null && activeWorkspaceFileProjectPreviewId !== null ? ( - ) : activeHostFilePath !== null ? ( activeRootHostFileThreadId && activeRootHostFileEnvironmentId ? ( - ) : ( - ) : ( - ) ) : undefined; + const fileTabContent = + fileTabContentBody === undefined ? undefined : ( + }> + {fileTabContentBody} + + ); const isBrowserTabActive = activeBrowserTab !== null; const rootPanelMetadataContent = useMemo( () => ( @@ -2760,16 +2776,15 @@ export function RootComposeView(props: RootComposeViewProps) { }, [openWorkspaceFile], ); - const rootPanelToggle = - !isSecondaryPanelOpen ? ( -
- -
- ) : null; + const rootPanelToggle = !isSecondaryPanelOpen ? ( +
+ +
+ ) : null; const attachmentsConfig = useMemo( () => ({ items: promptDraft.attachments, diff --git a/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx b/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx index 5539ab0a4..256ad41a0 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx @@ -1,17 +1,7 @@ -import { WorkerPoolContextProvider } from "@pierre/diffs/react"; -import { - createDiffWorker, - getDiffWorkerPoolSize, -} from "@/lib/diff-worker-pool"; +import DiffWorkerPoolProvider from "@/components/secondary-panel/DiffWorkerPoolProvider"; import { ThreadDetailView } from "./ThreadDetailView"; import type { ThreadRoutePathArgs } from "@/lib/route-paths"; -const WORKER_POOL_OPTIONS = { - workerFactory: createDiffWorker, - poolSize: getDiffWorkerPoolSize(), -}; -const HIGHLIGHTER_OPTIONS = {}; - interface ThreadDetailRoutePageProps { surface?: "page"; } @@ -40,12 +30,5 @@ export default function ThreadDetailRoute(props: ThreadDetailRouteProps) { ); - return ( - - {view} - - ); + return {view}; } diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 8a73c1f31..ac403d8bc 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -3,6 +3,7 @@ import { readFile, stat } from "node:fs/promises"; import { performance } from "node:perf_hooks"; import { extname, join, resolve } from "node:path"; import { Hono } from "hono"; +import { compress } from "hono/compress"; import { cors } from "hono/cors"; import { buildLocalAppOrigins, @@ -204,6 +205,7 @@ export function createApp( }, }), ); + app.use("*", compress()); app.onError((error) => errorToResponse(error, deps.logger)); app.get("/health", (context) => context.json({ ok: true })); app.use("/api/v1/*", async (context, next) => { diff --git a/apps/server/test/app/static-cache.test.ts b/apps/server/test/app/static-cache.test.ts index ed13996e5..4f5ea5db5 100644 --- a/apps/server/test/app/static-cache.test.ts +++ b/apps/server/test/app/static-cache.test.ts @@ -15,7 +15,7 @@ describe("production static cache headers", () => { ); await writeFile( join(staticDir, "assets", "index-test.js"), - "console.log('fresh bundle');", + `console.log('${"fresh bundle ".repeat(600)}');`, ); const harness = await createTestAppHarness(); @@ -34,6 +34,15 @@ describe("production static cache headers", () => { "public, max-age=31536000, immutable", ); + const compressedAssetResponse = await serverApp.app.request( + "/assets/index-test.js", + { headers: { "accept-encoding": "gzip" } }, + ); + expect(compressedAssetResponse.headers.get("content-encoding")).toBe( + "gzip", + ); + expect(compressedAssetResponse.headers.has("content-length")).toBe(false); + const apiMissResponse = await serverApp.app.request( "/api/v1/does-not-exist.js", ); diff --git a/scripts/measure-app-startup-payload.mjs b/scripts/measure-app-startup-payload.mjs new file mode 100644 index 000000000..01a52bd0f --- /dev/null +++ b/scripts/measure-app-startup-payload.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { gzipSync } from "node:zlib"; + +const DEFAULT_DIST_DIR = "apps/app/dist"; + +function usage() { + console.error( + "Usage: node scripts/measure-app-startup-payload.mjs [dist-dir]", + ); +} + +function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + const kib = bytes / 1024; + if (kib < 1024) return `${kib.toFixed(1)} KiB`; + return `${(kib / 1024).toFixed(2)} MiB`; +} + +function walkFiles(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const filePath = join(dir, entry.name); + return entry.isDirectory() ? walkFiles(filePath) : [filePath]; + }); +} + +function measureFile(filePath, label) { + const body = readFileSync(filePath); + return { + gzipBytes: gzipSync(body).length, + label, + rawBytes: body.length, + }; +} + +function extractInitialReferences(html) { + const references = []; + const pattern = /<(script|link)\b[^>]+(?:src|href)="([^"]+)"/g; + for (const match of html.matchAll(pattern)) { + const url = match[2]; + if (url.startsWith("/")) { + references.push(url); + } + } + return references; +} + +function sum(items, key) { + return items.reduce((total, item) => total + item[key], 0); +} + +function printTable(title, items) { + console.log(`\n${title}`); + for (const item of items) { + console.log( + `${formatBytes(item.rawBytes).padStart(10)} raw ${formatBytes( + item.gzipBytes, + ).padStart(10)} gzip ${item.label}`, + ); + } +} + +const args = process.argv.slice(2); +if (args.includes("-h") || args.includes("--help")) { + usage(); + process.exit(0); +} +if (args.length > 1) { + usage(); + process.exit(1); +} + +const distDir = resolve(args[0] ?? DEFAULT_DIST_DIR); +const indexPath = join(distDir, "index.html"); +if (!existsSync(indexPath)) { + console.error( + `Missing ${indexPath}. Run: pnpm exec turbo run build --filter=@bb/app`, + ); + process.exit(1); +} + +const indexHtml = readFileSync(indexPath, "utf8"); +const initialReferences = extractInitialReferences(indexHtml); +const initialFiles = [ + measureFile(indexPath, "/index.html"), + ...initialReferences + .map((reference) => { + const filePath = join(distDir, reference.slice(1)); + return existsSync(filePath) && statSync(filePath).isFile() + ? measureFile(filePath, reference) + : null; + }) + .filter(Boolean), +]; +const allFiles = walkFiles(distDir) + .filter((filePath) => !filePath.endsWith(".map")) + .map((filePath) => + measureFile(filePath, `/${filePath.slice(distDir.length + 1)}`), + ) + .sort((a, b) => b.rawBytes - a.rawBytes); + +console.log(`dist: ${distDir}`); +console.log(`initial references: ${initialReferences.length}`); +console.log( + `initial total: ${formatBytes(sum(initialFiles, "rawBytes"))} raw, ${formatBytes( + sum(initialFiles, "gzipBytes"), + )} gzip`, +); +console.log( + `all non-map files: ${formatBytes(sum(allFiles, "rawBytes"))} raw, ${formatBytes( + sum(allFiles, "gzipBytes"), + )} gzip`, +); + +printTable( + "Largest initial files", + [...initialFiles].sort((a, b) => b.rawBytes - a.rawBytes).slice(0, 20), +); +printTable("Largest app files", allFiles.slice(0, 20)); From 84cfafdd070ed767d0b89616bb62ea5e4fce323c Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Sun, 28 Jun 2026 11:19:47 -0700 Subject: [PATCH 2/5] Fix root compose secondary panel lazy test --- .../RootComposeSecondaryContent.test.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/app/src/views/RootComposeSecondaryContent.test.tsx b/apps/app/src/views/RootComposeSecondaryContent.test.tsx index e97e774a8..bd92476aa 100644 --- a/apps/app/src/views/RootComposeSecondaryContent.test.tsx +++ b/apps/app/src/views/RootComposeSecondaryContent.test.tsx @@ -85,6 +85,15 @@ vi.mock("@/components/ui/responsive-overlay.js", async () => { return { ResponsiveDrawerShell }; }); +vi.mock("@/components/secondary-panel/DiffWorkerPoolProvider", async () => { + const React = await import("react"); + + const DiffWorkerPoolProvider = ({ children }: { children?: ReactNode }) => + React.createElement(React.Fragment, null, children); + + return { default: DiffWorkerPoolProvider }; +}); + vi.mock("@/components/secondary-panel/ThreadSecondaryPanel", async () => { const React = await import("react"); @@ -174,7 +183,7 @@ afterEach(() => { }); describe("RootComposeSecondaryContent desktop layout", () => { - it("syncs the panel group when persisted open state arrives after mount", () => { + it("syncs the panel group when persisted open state arrives after mount", async () => { const view = renderRootCompose({ isCompactViewport: false, isSecondaryPanelOpen: false, @@ -188,7 +197,9 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).toHaveBeenCalledTimes(1); expect(panelGroupState.setLayout).toHaveBeenLastCalledWith([60, 40]); expect( - screen.getByTestId("inline-secondary-panel").getAttribute("data-open"), + (await screen.findByTestId("inline-secondary-panel")).getAttribute( + "data-open", + ), ).toBe("true"); }); @@ -207,7 +218,7 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).toHaveBeenLastCalledWith([100, 0]); }); - it("leaves the panel group alone while the root panel renders as a drawer", () => { + it("leaves the panel group alone while the root panel renders as a drawer", async () => { const view = renderRootCompose({ isCompactViewport: true, isSecondaryPanelOpen: false, @@ -219,7 +230,9 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).not.toHaveBeenCalled(); expect( - screen.getByTestId("drawer-secondary-panel").getAttribute("data-open"), + (await screen.findByTestId("drawer-secondary-panel")).getAttribute( + "data-open", + ), ).toBe("true"); }); }); From 15993cb111bec0bf6129e9374a26bf485c166377 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Sun, 28 Jun 2026 11:45:22 -0700 Subject: [PATCH 3/5] Remove ineffective startup code splitting --- apps/app/src/App.tsx | 18 +- .../DiffWorkerPoolProvider.tsx | 27 --- .../RootComposeSecondaryContent.test.tsx | 21 +- .../src/views/RootComposeSecondaryContent.tsx | 98 +++------ apps/app/src/views/RootComposeView.tsx | 189 ++++++++---------- .../views/thread-detail/ThreadDetailRoute.tsx | 21 +- scripts/measure-app-startup-payload.mjs | 2 + 7 files changed, 140 insertions(+), 236 deletions(-) delete mode 100644 apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index f6ed82396..858671fb1 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -1,7 +1,6 @@ import { lazy, Suspense, useEffect } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { AppLayout } from "./components/layout/AppLayout"; -import { PageShell } from "./components/ui/page-shell"; import { AuthCallbackView } from "./views/AuthCallbackView"; import { RootComposeRoute } from "./views/RootComposeView"; import { QuickCreateProjectProvider } from "./hooks/useQuickCreateProject"; @@ -99,11 +98,14 @@ function PopoutRouteFallback() { function AppRoutes() { return ( - }> + } /> } /> - } /> + } + /> } @@ -142,16 +144,6 @@ function AppRoutes() { ); } -function AppRouteFallback() { - return ( - -

- Loading... -

-
- ); -} - export function App() { // Connect WebSocket for real-time invalidation useWebSocket(); diff --git a/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx b/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx deleted file mode 100644 index 3f3eb0b52..000000000 --- a/apps/app/src/components/secondary-panel/DiffWorkerPoolProvider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReactNode } from "react"; -import { WorkerPoolContextProvider } from "@pierre/diffs/react"; -import { - createDiffWorker, - getDiffWorkerPoolSize, -} from "@/lib/diff-worker-pool"; - -const WORKER_POOL_OPTIONS = { - workerFactory: createDiffWorker, - poolSize: getDiffWorkerPoolSize(), -}; -const HIGHLIGHTER_OPTIONS = {}; - -export default function DiffWorkerPoolProvider({ - children, -}: { - children: ReactNode; -}) { - return ( - - {children} - - ); -} diff --git a/apps/app/src/views/RootComposeSecondaryContent.test.tsx b/apps/app/src/views/RootComposeSecondaryContent.test.tsx index bd92476aa..e97e774a8 100644 --- a/apps/app/src/views/RootComposeSecondaryContent.test.tsx +++ b/apps/app/src/views/RootComposeSecondaryContent.test.tsx @@ -85,15 +85,6 @@ vi.mock("@/components/ui/responsive-overlay.js", async () => { return { ResponsiveDrawerShell }; }); -vi.mock("@/components/secondary-panel/DiffWorkerPoolProvider", async () => { - const React = await import("react"); - - const DiffWorkerPoolProvider = ({ children }: { children?: ReactNode }) => - React.createElement(React.Fragment, null, children); - - return { default: DiffWorkerPoolProvider }; -}); - vi.mock("@/components/secondary-panel/ThreadSecondaryPanel", async () => { const React = await import("react"); @@ -183,7 +174,7 @@ afterEach(() => { }); describe("RootComposeSecondaryContent desktop layout", () => { - it("syncs the panel group when persisted open state arrives after mount", async () => { + it("syncs the panel group when persisted open state arrives after mount", () => { const view = renderRootCompose({ isCompactViewport: false, isSecondaryPanelOpen: false, @@ -197,9 +188,7 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).toHaveBeenCalledTimes(1); expect(panelGroupState.setLayout).toHaveBeenLastCalledWith([60, 40]); expect( - (await screen.findByTestId("inline-secondary-panel")).getAttribute( - "data-open", - ), + screen.getByTestId("inline-secondary-panel").getAttribute("data-open"), ).toBe("true"); }); @@ -218,7 +207,7 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).toHaveBeenLastCalledWith([100, 0]); }); - it("leaves the panel group alone while the root panel renders as a drawer", async () => { + it("leaves the panel group alone while the root panel renders as a drawer", () => { const view = renderRootCompose({ isCompactViewport: true, isSecondaryPanelOpen: false, @@ -230,9 +219,7 @@ describe("RootComposeSecondaryContent desktop layout", () => { expect(panelGroupState.setLayout).not.toHaveBeenCalled(); expect( - (await screen.findByTestId("drawer-secondary-panel")).getAttribute( - "data-open", - ), + screen.getByTestId("drawer-secondary-panel").getAttribute("data-open"), ).toBe("true"); }); }); diff --git a/apps/app/src/views/RootComposeSecondaryContent.tsx b/apps/app/src/views/RootComposeSecondaryContent.tsx index 40b0394a2..0ba705f62 100644 --- a/apps/app/src/views/RootComposeSecondaryContent.tsx +++ b/apps/app/src/views/RootComposeSecondaryContent.tsx @@ -1,12 +1,11 @@ import { - lazy, - Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, + type ComponentProps, type ReactNode, } from "react"; import { useAtomValue } from "jotai"; @@ -17,7 +16,7 @@ import { } from "react-resizable-panels"; import { ResponsiveDrawerShell } from "@/components/ui/responsive-overlay.js"; import { useIsCompactViewport } from "@/components/ui/hooks/use-compact-viewport.js"; -import type { ThreadSecondaryPanelProps } from "@/components/secondary-panel/ThreadSecondaryPanel"; +import { ThreadSecondaryPanel } from "@/components/secondary-panel/ThreadSecondaryPanel"; import { secondaryPanelWidthPercentAtom } from "@/components/secondary-panel/threadSecondaryPanelAtoms"; import { PANEL_COLLAPSE_TRANSITION_CLASS } from "@/components/secondary-panel/panelTransitionTokens"; import { PAGE_SHELL_CONTENT_STYLE } from "@/components/ui/page-shell-content-style.js"; @@ -27,17 +26,9 @@ import { cn } from "@/lib/utils"; const CLOSED_MAIN_PANEL_SIZE_PERCENT = 100; const MAIN_PANEL_MIN_SIZE_PERCENT = 30; const ROOT_COMPOSE_MAX_WIDTH_CLASS = "max-w-[760px]"; -const LazyThreadSecondaryPanel = lazy(() => - import("@/components/secondary-panel/ThreadSecondaryPanel").then( - (module) => ({ default: module.ThreadSecondaryPanel }), - ), -); -const LazyDiffWorkerPoolProvider = lazy( - () => import("@/components/secondary-panel/DiffWorkerPoolProvider"), -); type RootSecondaryPanelProps = Omit< - ThreadSecondaryPanelProps, + ComponentProps, | "browserDeck" | "isConversationCollapsed" | "onToggleConversationCollapse" @@ -58,28 +49,6 @@ interface RootComposeSecondaryContentProps { function noopToggleConversationCollapse(): void {} -function RootComposeDiffWorkerPoolBoundary({ - children, - fallback, -}: { - children: ReactNode; - fallback: ReactNode; -}) { - return ( - - {children} - - ); -} - -function RootComposeSecondaryPanelLoading() { - return ( -
- Loading... -
- ); -} - export function RootComposeSecondaryContent({ children, contentClassName, @@ -207,47 +176,26 @@ export function RootComposeSecondaryContent({ secondaryWidth, ]); }, [isSecondaryPanelOpen, renderAsDrawer]); - const inlineSecondaryPanelFallback = ( - - - - ); - const inlineSecondaryPanelContent = - !renderAsDrawer && isSecondaryPanelOpen ? ( - - - - ) : null; - const drawerSecondaryPanelContent = - renderAsDrawer && isSecondaryPanelOpen ? ( - } - > - - - ) : null; + const inlineSecondaryPanelContent = !renderAsDrawer ? ( + + ) : null; + const drawerSecondaryPanelContent = renderAsDrawer ? ( + + ) : null; return (
diff --git a/apps/app/src/views/RootComposeView.tsx b/apps/app/src/views/RootComposeView.tsx index 02d5c4dc4..fb5a1e059 100644 --- a/apps/app/src/views/RootComposeView.tsx +++ b/apps/app/src/views/RootComposeView.tsx @@ -1,6 +1,4 @@ import { - lazy, - Suspense, useCallback, useEffect, useMemo, @@ -9,6 +7,7 @@ import { type ReactNode, } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { WorkerPoolContextProvider } from "@pierre/diffs/react"; import { findLocalPathProjectSourceForHost, type EnvironmentStatus, @@ -49,6 +48,13 @@ import type { ProjectSelectorOption } from "@/components/pickers/ProjectSelector import type { ReuseThreadOption } from "@/components/pickers/WorktreePicker"; import { HEADER_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; import type { SecondaryPanelFileTab } from "@/components/secondary-panel/ThreadSecondaryPanel"; +import { FilePreview } from "@/components/secondary-panel/FilePreview"; +import { + HostFilePreviewTabContent, + ProjectFilePreviewTabContent, + ThreadStorageFilePreviewTabContent, + WorkspaceFilePreviewTabContent, +} from "@/components/secondary-panel/ThreadSecondaryPanelTabContent"; import { BrowserTabDeck } from "@/components/secondary-panel/BrowserTabDeck"; import { NewTabPage } from "@/components/secondary-panel/NewTabPage"; import { EmptyStatePanel } from "@/components/ui/empty-state"; @@ -75,7 +81,9 @@ import { } from "@/hooks/queries/project-queries"; import { useEnvironment } from "@/hooks/queries/environment-queries"; import { useProjectDefaultExecutionOptions } from "@/hooks/queries/project-default-execution-options-query"; -import { useHostProviderCliStatus } from "@/hooks/queries/system-queries"; +import { + useHostProviderCliStatus, +} from "@/hooks/queries/system-queries"; import { useSidebarNavigation } from "@/hooks/queries/sidebar-navigation-query"; import { useThreads } from "@/hooks/queries/thread-queries"; import { useCommandSuggestions } from "@/hooks/useCommandSuggestions"; @@ -162,6 +170,7 @@ import { type FileSearchSelection, } from "@/components/secondary-panel/useThreadFileTabs"; import { resolveRightPanelFileVisual } from "@/components/secondary-panel/rightPanelFileVisuals"; +import { ThreadTerminalPanel } from "@/components/thread/terminal/ThreadTerminalPanel"; import { DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS, @@ -184,6 +193,10 @@ import { resolveEnvironmentOpenContext, resolveThreadWorkspacePreviewRootPath, } from "./thread-detail/threadWorkspaceOpenPath"; +import { + createDiffWorker, + getDiffWorkerPoolSize, +} from "@/lib/diff-worker-pool"; const ROOT_COMPOSE_ZEN_MODE_STORAGE_KEY = "bb.promptbox.zen-mode.root-compose"; const ROOT_COMPOSE_SIDEBAR_ACTION_ALIGNED_TOP_PADDING_CLASS = "pt-14"; @@ -210,36 +223,11 @@ const ROOT_COMPOSE_EMPTY_WELCOME_CONTENT_CLASS = "min-h-full flex-1 items-center justify-center pb-12"; const ROOT_COMPOSE_FIXED_PANEL_STATE_ID = "root-compose"; const EMPTY_TERMINAL_SESSIONS: readonly TerminalSession[] = []; -const LazyFilePreview = lazy(() => - import("@/components/secondary-panel/FilePreview").then((module) => ({ - default: module.FilePreview, - })), -); -const LazyWorkspaceFilePreviewTabContent = lazy(() => - import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( - (module) => ({ default: module.WorkspaceFilePreviewTabContent }), - ), -); -const LazyProjectFilePreviewTabContent = lazy(() => - import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( - (module) => ({ default: module.ProjectFilePreviewTabContent }), - ), -); -const LazyHostFilePreviewTabContent = lazy(() => - import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( - (module) => ({ default: module.HostFilePreviewTabContent }), - ), -); -const LazyThreadStorageFilePreviewTabContent = lazy(() => - import("@/components/secondary-panel/ThreadSecondaryPanelTabContent").then( - (module) => ({ default: module.ThreadStorageFilePreviewTabContent }), - ), -); -const LazyThreadTerminalPanel = lazy(() => - import("@/components/thread/terminal/ThreadTerminalPanel").then((module) => ({ - default: module.ThreadTerminalPanel, - })), -); +const FILE_PREVIEW_WORKER_POOL_OPTIONS = { + workerFactory: createDiffWorker, + poolSize: getDiffWorkerPoolSize(), +}; +const FILE_PREVIEW_HIGHLIGHTER_OPTIONS = {}; type ProjectSelectionChangeHandler = NewThreadProjectConfig["onChange"]; type SecondaryPanelChangeHandler = (panel: ThreadSecondaryPanelTab) => void; @@ -740,9 +728,7 @@ export function buildRootComposeTerminalSessions({ environmentTerminalSessions, globalTerminalSessions, terminalTarget, -}: BuildRootComposeTerminalSessionsArgs): - | readonly TerminalSession[] - | undefined { +}: BuildRootComposeTerminalSessionsArgs): readonly TerminalSession[] | undefined { if (terminalTarget?.kind === "environment") { return environmentTerminalSessions; } @@ -833,14 +819,6 @@ function CodexCliVersionBanner({ ); } -function RootComposeFileTabLoading() { - return ( - - Loading... - - ); -} - export function RootComposeRoute() { const { projectId } = useParams<{ projectId: string }>(); @@ -848,7 +826,14 @@ export function RootComposeRoute() { return ; } - return ; + return ( + + + + ); } export function RootComposeView(props: RootComposeViewProps) { @@ -913,9 +898,7 @@ export function RootComposeView(props: RootComposeViewProps) { const primaryHost = useMemo(() => { const hosts = hostsQuery.data; if (!hosts || hosts.length === 0) return null; - return ( - hosts.find((host) => host.status === "connected") ?? hosts[0] ?? null - ); + return hosts.find((host) => host.status === "connected") ?? hosts[0] ?? null; }, [hostsQuery.data]); const primaryHostId = primaryHost?.id ?? null; const uploadPromptAttachment = useUploadPromptAttachment(); @@ -1849,20 +1832,21 @@ export function RootComposeView(props: RootComposeViewProps) { const shouldUseRootStorageViewerForActiveTab = rawActiveRootStorageFileThreadId !== null && rawActiveRootStorageFileThreadId === rootPanelThreadId; - const { threadStorageRootPath: activeStorageThreadStorageRootPath } = - useThreadStorageViewer({ - activePath: null, - fileListEnabled: - props.surface === "page" && - rawActiveRootStorageFileThreadId !== null && - !shouldUseRootStorageViewerForActiveTab, - filePreviewEnabled: false, - threadId: - rawActiveRootStorageFileThreadId !== null && - !shouldUseRootStorageViewerForActiveTab - ? rawActiveRootStorageFileThreadId - : undefined, - }); + const { + threadStorageRootPath: activeStorageThreadStorageRootPath, + } = useThreadStorageViewer({ + activePath: null, + fileListEnabled: + props.surface === "page" && + rawActiveRootStorageFileThreadId !== null && + !shouldUseRootStorageViewerForActiveTab, + filePreviewEnabled: false, + threadId: + rawActiveRootStorageFileThreadId !== null && + !shouldUseRootStorageViewerForActiveTab + ? rawActiveRootStorageFileThreadId + : undefined, + }); const activeStorageFileRootPath = shouldUseRootStorageViewerForActiveTab ? rootThreadStorageRootPath : activeStorageThreadStorageRootPath; @@ -1893,8 +1877,7 @@ export function RootComposeView(props: RootComposeViewProps) { const loadedTerminalSessions = useMemo( () => buildRootComposeTerminalSessions({ - environmentTerminalSessions: - environmentTerminalsListQuery.data?.sessions, + environmentTerminalSessions: environmentTerminalsListQuery.data?.sessions, globalTerminalSessions: globalTerminalsListQuery.data?.sessions, terminalTarget: rootPanelTerminalTarget, }), @@ -1904,7 +1887,8 @@ export function RootComposeView(props: RootComposeViewProps) { rootPanelTerminalTarget, ], ); - const terminalSessions = loadedTerminalSessions ?? EMPTY_TERMINAL_SESSIONS; + const terminalSessions = + loadedTerminalSessions ?? EMPTY_TERMINAL_SESSIONS; const terminalsListLoaded = loadedTerminalSessions !== undefined; const activeTerminalCount = useMemo( () => @@ -2023,12 +2007,13 @@ export function RootComposeView(props: RootComposeViewProps) { const closeRootSecondaryPanel = useCallback(() => { setRootSecondaryPanelForSurface(null); }, [setRootSecondaryPanelForSurface]); - const openRootSecondaryPanel = useCallback( - (panel) => { - setRootSecondaryPanelForSurface(panel); - }, - [setRootSecondaryPanelForSurface], - ); + const openRootSecondaryPanel = + useCallback( + (panel) => { + setRootSecondaryPanelForSurface(panel); + }, + [setRootSecondaryPanelForSurface], + ); const toggleRootPersistedSecondaryPanel = useCallback(() => { if (isPersistedSecondaryPanelOpen) { closeRootSecondaryPanel(); @@ -2198,7 +2183,11 @@ export function RootComposeView(props: RootComposeViewProps) { }); }, [browserTabIds, openBrowserTabAndReveal]); const renderBrowserDeck = useCallback( - ({ canShowNativeBrowserView }: { canShowNativeBrowserView: boolean }) => { + ({ + canShowNativeBrowserView, + }: { + canShowNativeBrowserView: boolean; + }) => { if (rootPanelThreadId === null) { return null; } @@ -2247,13 +2236,14 @@ export function RootComposeView(props: RootComposeViewProps) { } handleOpenNewTab(); }, [closeSecondaryPanel, handleOpenNewTab, isSecondaryPanelOpen]); - const handleSecondaryPanelChange = useCallback( - (panel) => { - clearActiveFileTabs(); - openSecondaryPanel(panel); - }, - [clearActiveFileTabs, openSecondaryPanel], - ); + const handleSecondaryPanelChange = + useCallback( + (panel) => { + clearActiveFileTabs(); + openSecondaryPanel(panel); + }, + [clearActiveFileTabs, openSecondaryPanel], + ); const handleSecondaryPanelFocus = useCallback(() => { touchFixedPanelTabsState(); }, [touchFixedPanelTabsState]); @@ -2664,9 +2654,9 @@ export function RootComposeView(props: RootComposeViewProps) { activeTabId: activeFixedSecondaryTabId, tabs: syncedOrderedSecondaryFileTabs, }); - const fileTabContentBody: ReactNode = + const fileTabContent: ReactNode = activeTerminalId && rootPanelTerminalTarget ? ( - ) : activeWorkspaceFilePath !== null && activeWorkspaceFileEnvironmentId !== null ? ( - ) : activeWorkspaceFilePath !== null && activeWorkspaceFileProjectPreviewId !== null ? ( - ) : activeHostFilePath !== null ? ( activeRootHostFileThreadId && activeRootHostFileEnvironmentId ? ( - ) : ( - ) : ( - ) ) : undefined; - const fileTabContent = - fileTabContentBody === undefined ? undefined : ( - }> - {fileTabContentBody} - - ); const isBrowserTabActive = activeBrowserTab !== null; const rootPanelMetadataContent = useMemo( () => ( @@ -2776,15 +2760,16 @@ export function RootComposeView(props: RootComposeViewProps) { }, [openWorkspaceFile], ); - const rootPanelToggle = !isSecondaryPanelOpen ? ( -
- -
- ) : null; + const rootPanelToggle = + !isSecondaryPanelOpen ? ( +
+ +
+ ) : null; const attachmentsConfig = useMemo( () => ({ items: promptDraft.attachments, diff --git a/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx b/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx index 256ad41a0..5539ab0a4 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailRoute.tsx @@ -1,7 +1,17 @@ -import DiffWorkerPoolProvider from "@/components/secondary-panel/DiffWorkerPoolProvider"; +import { WorkerPoolContextProvider } from "@pierre/diffs/react"; +import { + createDiffWorker, + getDiffWorkerPoolSize, +} from "@/lib/diff-worker-pool"; import { ThreadDetailView } from "./ThreadDetailView"; import type { ThreadRoutePathArgs } from "@/lib/route-paths"; +const WORKER_POOL_OPTIONS = { + workerFactory: createDiffWorker, + poolSize: getDiffWorkerPoolSize(), +}; +const HIGHLIGHTER_OPTIONS = {}; + interface ThreadDetailRoutePageProps { surface?: "page"; } @@ -30,5 +40,12 @@ export default function ThreadDetailRoute(props: ThreadDetailRouteProps) { ); - return {view}; + return ( + + {view} + + ); } diff --git a/scripts/measure-app-startup-payload.mjs b/scripts/measure-app-startup-payload.mjs index 01a52bd0f..292d3ec87 100644 --- a/scripts/measure-app-startup-payload.mjs +++ b/scripts/measure-app-startup-payload.mjs @@ -93,6 +93,7 @@ const initialFiles = [ }) .filter(Boolean), ]; +const initialRequestCount = initialFiles.length; const allFiles = walkFiles(distDir) .filter((filePath) => !filePath.endsWith(".map")) .map((filePath) => @@ -102,6 +103,7 @@ const allFiles = walkFiles(distDir) console.log(`dist: ${distDir}`); console.log(`initial references: ${initialReferences.length}`); +console.log(`initial requests: ${initialRequestCount}`); console.log( `initial total: ${formatBytes(sum(initialFiles, "rawBytes"))} raw, ${formatBytes( sum(initialFiles, "gzipBytes"), From 5c0feacaf4006ba48aa24b3a9c7c8aa17f5c4b4a Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Sun, 28 Jun 2026 12:51:42 -0700 Subject: [PATCH 4/5] Serve precompressed app assets --- apps/app/package.json | 2 +- apps/server/src/server.ts | 117 ++++++ apps/server/test/app/static-cache.test.ts | 55 ++- scripts/measure-app-startup-payload.mjs | 26 +- scripts/measure-browser-startup.mjs | 414 ++++++++++++++++++++++ scripts/precompress-app-dist.mjs | 118 ++++++ 6 files changed, 721 insertions(+), 11 deletions(-) create mode 100644 scripts/measure-browser-startup.mjs create mode 100644 scripts/precompress-app-dist.mjs diff --git a/apps/app/package.json b/apps/app/package.json index dbf260a85..19ff5115c 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "vite --config vite.dev.config.ts --configLoader runner", - "build": "vite build --logLevel warn", + "build": "vite build --logLevel warn && node ../../scripts/precompress-app-dist.mjs dist", "preview": "vite preview", "clean": "rimraf dist", "storybook": "ladle serve", diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index ac403d8bc..021d3d0ef 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -101,6 +101,8 @@ interface CreateAppOptions { } interface StaticResponseHeadersArgs { + contentEncoding?: string; + contentLength?: number; contentType: string; urlPath: string; } @@ -113,6 +115,10 @@ const WEB_SOCKET_SHUTDOWN_REASON = "server-shutdown"; const SLOW_API_REQUEST_LOG_THRESHOLD_MS = 1_000; const THREAD_EVENT_WAIT_PATH_PATTERN = /^\/api\/v1\/threads\/[^/]+\/events\/wait$/u; +const PRECOMPRESSED_STATIC_FILES = [ + { encoding: "br", extension: ".br" }, + { encoding: "gzip", extension: ".gz" }, +] as const; interface ShouldLogSlowApiRequestArgs { durationMs: number; @@ -136,9 +142,103 @@ function createStaticResponseHeaders(args: StaticResponseHeadersArgs): Headers { ? STATIC_ASSET_CACHE_CONTROL : STATIC_INDEX_CACHE_CONTROL, ); + if (args.contentEncoding !== undefined) { + headers.set("content-encoding", args.contentEncoding); + headers.set("vary", "Accept-Encoding"); + } + if (args.contentLength !== undefined) { + headers.set("content-length", String(args.contentLength)); + } return headers; } +function acceptedEncodingQuality( + acceptEncodingHeader: string | undefined, + encoding: string, +): number { + if (acceptEncodingHeader === undefined) { + return 0; + } + let wildcardQuality = 0; + for (const part of acceptEncodingHeader.split(",")) { + const [rawName, ...rawParams] = part.trim().split(";"); + const name = rawName?.trim().toLowerCase(); + const qParam = rawParams + .map((param) => param.trim().toLowerCase()) + .find((param) => param.startsWith("q=")); + const quality = + qParam === undefined + ? 1 + : Number.isNaN(Number(qParam.slice(2))) + ? 1 + : Number(qParam.slice(2)); + if (name === encoding) { + return quality; + } + if (name === "*") { + wildcardQuality = quality; + } + } + return wildcardQuality; +} + +function canServePrecompressedStaticFile(contentType: string): boolean { + return ( + contentType.startsWith("text/") || + contentType === "application/javascript" || + contentType === "application/json" || + contentType === "application/manifest+json" || + contentType === "application/wasm" || + contentType === "application/xml" || + contentType === "image/svg+xml" + ); +} + +async function findPrecompressedStaticFile(args: { + acceptEncodingHeader: string | undefined; + contentType: string; + filePath: string; +}): Promise<{ + contentLength: number; + encoding: string; + filePath: string; +} | null> { + if (!canServePrecompressedStaticFile(args.contentType)) { + return null; + } + + const candidates = PRECOMPRESSED_STATIC_FILES.map((candidate, index) => ({ + ...candidate, + index, + quality: acceptedEncodingQuality( + args.acceptEncodingHeader, + candidate.encoding, + ), + })) + .filter((candidate) => candidate.quality > 0) + .sort( + (left, right) => right.quality - left.quality || left.index - right.index, + ); + + for (const candidate of candidates) { + const encodedFilePath = `${args.filePath}${candidate.extension}`; + try { + const encodedStat = await stat(encodedFilePath); + if (encodedStat.isFile()) { + return { + contentLength: encodedStat.size, + encoding: candidate.encoding, + filePath: encodedFilePath, + }; + } + } catch { + // Sidecar missing — try the next acceptable encoding. + } + } + + return null; +} + function buildAllowedCorsOrigins(deps: AppDeps): Set { const originArgs: BuildLocalAppOriginsArgs = { serverPort: deps.config.serverPort, @@ -377,6 +477,7 @@ export function createApp( ".woff": "font/woff", ".woff2": "font/woff2", ".webp": "image/webp", + ".webmanifest": "application/manifest+json", ".map": "application/json", }; @@ -405,6 +506,22 @@ export function createApp( headers: createStaticResponseHeaders({ contentType, urlPath }), }); } + const precompressedFile = await findPrecompressedStaticFile({ + acceptEncodingHeader: context.req.header("accept-encoding"), + contentType, + filePath, + }); + if (precompressedFile !== null) { + const content = await readFile(precompressedFile.filePath); + return new Response(content, { + headers: createStaticResponseHeaders({ + contentEncoding: precompressedFile.encoding, + contentLength: precompressedFile.contentLength, + contentType, + urlPath, + }), + }); + } const content = await readFile(filePath); return new Response(content, { headers: createStaticResponseHeaders({ contentType, urlPath }), diff --git a/apps/server/test/app/static-cache.test.ts b/apps/server/test/app/static-cache.test.ts index 4f5ea5db5..c96401649 100644 --- a/apps/server/test/app/static-cache.test.ts +++ b/apps/server/test/app/static-cache.test.ts @@ -1,3 +1,4 @@ +import { brotliCompressSync, gzipSync } from "node:zlib"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -13,9 +14,16 @@ describe("production static cache headers", () => { join(staticDir, "index.html"), '', ); + const bundleBody = `console.log('${"fresh bundle ".repeat(600)}');`; + const bundlePath = join(staticDir, "assets", "index-test.js"); + const brotliBundle = brotliCompressSync(Buffer.from(bundleBody)); + const gzipBundle = gzipSync(Buffer.from(bundleBody)); + await writeFile(bundlePath, bundleBody); + await writeFile(`${bundlePath}.br`, brotliBundle); + await writeFile(`${bundlePath}.gz`, gzipBundle); await writeFile( - join(staticDir, "assets", "index-test.js"), - `console.log('${"fresh bundle ".repeat(600)}');`, + join(staticDir, "assets", "dynamic-only.js"), + `console.log('${"dynamic bundle ".repeat(600)}');`, ); const harness = await createTestAppHarness(); @@ -34,14 +42,51 @@ describe("production static cache headers", () => { "public, max-age=31536000, immutable", ); - const compressedAssetResponse = await serverApp.app.request( + const brotliAssetResponse = await serverApp.app.request( + "/assets/index-test.js", + { headers: { "accept-encoding": "br, gzip" } }, + ); + expect(brotliAssetResponse.headers.get("content-encoding")).toBe("br"); + expect( + brotliAssetResponse.headers + .get("vary") + ?.split(",") + .map((value) => value.trim()), + ).toContain("Accept-Encoding"); + expect(brotliAssetResponse.headers.get("content-length")).toBe( + String(brotliBundle.length), + ); + expect((await brotliAssetResponse.arrayBuffer()).byteLength).toBe( + brotliBundle.length, + ); + + const gzipAssetResponse = await serverApp.app.request( "/assets/index-test.js", { headers: { "accept-encoding": "gzip" } }, ); - expect(compressedAssetResponse.headers.get("content-encoding")).toBe( + expect(gzipAssetResponse.headers.get("content-encoding")).toBe("gzip"); + expect(gzipAssetResponse.headers.get("content-length")).toBe( + String(gzipBundle.length), + ); + + const gzipPreferredAssetResponse = await serverApp.app.request( + "/assets/index-test.js", + { headers: { "accept-encoding": "br;q=0, gzip;q=1" } }, + ); + expect(gzipPreferredAssetResponse.headers.get("content-encoding")).toBe( "gzip", ); - expect(compressedAssetResponse.headers.has("content-length")).toBe(false); + + const dynamicCompressedAssetResponse = await serverApp.app.request( + "/assets/dynamic-only.js", + { headers: { "accept-encoding": "gzip" } }, + ); + expect( + dynamicCompressedAssetResponse.headers.get("content-encoding"), + ).toBe("gzip"); + expect(dynamicCompressedAssetResponse.headers.has("content-length")).toBe( + false, + ); const apiMissResponse = await serverApp.app.request( "/api/v1/does-not-exist.js", diff --git a/scripts/measure-app-startup-payload.mjs b/scripts/measure-app-startup-payload.mjs index 292d3ec87..c5d827ff3 100644 --- a/scripts/measure-app-startup-payload.mjs +++ b/scripts/measure-app-startup-payload.mjs @@ -1,7 +1,11 @@ #!/usr/bin/env node import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; -import { gzipSync } from "node:zlib"; +import { + constants as zlibConstants, + brotliCompressSync, + gzipSync, +} from "node:zlib"; const DEFAULT_DIST_DIR = "apps/app/dist"; @@ -28,6 +32,11 @@ function walkFiles(dir) { function measureFile(filePath, label) { const body = readFileSync(filePath); return { + brotliBytes: brotliCompressSync(body, { + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: 10, + }, + }).length, gzipBytes: gzipSync(body).length, label, rawBytes: body.length, @@ -56,7 +65,9 @@ function printTable(title, items) { console.log( `${formatBytes(item.rawBytes).padStart(10)} raw ${formatBytes( item.gzipBytes, - ).padStart(10)} gzip ${item.label}`, + ).padStart(10)} gzip ${formatBytes(item.brotliBytes).padStart( + 10, + )} br ${item.label}`, ); } } @@ -95,7 +106,12 @@ const initialFiles = [ ]; const initialRequestCount = initialFiles.length; const allFiles = walkFiles(distDir) - .filter((filePath) => !filePath.endsWith(".map")) + .filter( + (filePath) => + !filePath.endsWith(".br") && + !filePath.endsWith(".gz") && + !filePath.endsWith(".map"), + ) .map((filePath) => measureFile(filePath, `/${filePath.slice(distDir.length + 1)}`), ) @@ -107,12 +123,12 @@ console.log(`initial requests: ${initialRequestCount}`); console.log( `initial total: ${formatBytes(sum(initialFiles, "rawBytes"))} raw, ${formatBytes( sum(initialFiles, "gzipBytes"), - )} gzip`, + )} gzip, ${formatBytes(sum(initialFiles, "brotliBytes"))} br`, ); console.log( `all non-map files: ${formatBytes(sum(allFiles, "rawBytes"))} raw, ${formatBytes( sum(allFiles, "gzipBytes"), - )} gzip`, + )} gzip, ${formatBytes(sum(allFiles, "brotliBytes"))} br`, ); printTable( diff --git a/scripts/measure-browser-startup.mjs b/scripts/measure-browser-startup.mjs new file mode 100644 index 000000000..fb2053a80 --- /dev/null +++ b/scripts/measure-browser-startup.mjs @@ -0,0 +1,414 @@ +#!/usr/bin/env node +import { createServer } from "node:http"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import { extname, relative, resolve, sep } from "node:path"; +import { spawn } from "node:child_process"; + +const DEV_BROWSER_PACKAGE = "dev-browser@0.2.8"; +const DEFAULT_BROWSER_NAME = "bb-startup-measure"; +const DEFAULT_TIMEOUT_SECONDS = 90; +const DEFAULT_SETTLE_MS = 2_000; +const PROFILES = { + none: null, + airplane: { + downloadKibPerSecond: 80, + latencyMs: 600, + uploadKibPerSecond: 30, + }, +}; +const MIME = { + ".css": "text/css", + ".html": "text/html", + ".ico": "image/x-icon", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webmanifest": "application/manifest+json", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; +const PRECOMPRESSED = [ + { encoding: "br", extension: ".br" }, + { encoding: "gzip", extension: ".gz" }, +]; + +function usage() { + console.error(`Usage: + node scripts/measure-browser-startup.mjs [--profile none|airplane] + node scripts/measure-browser-startup.mjs --dist apps/app/dist [--profile none|airplane] + +Options: + --browser-name dev-browser instance name (default: ${DEFAULT_BROWSER_NAME}) + --settle-ms wait after DOMContentLoaded before collecting metrics + --timeout dev-browser script timeout (default: ${DEFAULT_TIMEOUT_SECONDS}) + +By default this runs: npx -y ${DEV_BROWSER_PACKAGE} --headless +Set DEV_BROWSER_BIN=dev-browser to use an installed binary instead.`); +} + +function takeOption(args, name) { + const index = args.indexOf(name); + if (index === -1) { + return undefined; + } + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) { + throw new Error(`Missing value for ${name}`); + } + args.splice(index, 2); + return value; +} + +function parseArgs() { + const args = process.argv.slice(2); + if (args.includes("-h") || args.includes("--help")) { + usage(); + process.exit(0); + } + + const browserName = + takeOption(args, "--browser-name") ?? DEFAULT_BROWSER_NAME; + const distDir = takeOption(args, "--dist"); + const profileName = takeOption(args, "--profile") ?? "none"; + const settleMs = Number(takeOption(args, "--settle-ms") ?? DEFAULT_SETTLE_MS); + const timeoutSeconds = Number( + takeOption(args, "--timeout") ?? DEFAULT_TIMEOUT_SECONDS, + ); + + if (!(profileName in PROFILES)) { + throw new Error(`Unknown profile "${profileName}"`); + } + if (!Number.isFinite(settleMs) || settleMs < 0) { + throw new Error("--settle-ms must be a non-negative number"); + } + if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) { + throw new Error("--timeout must be a positive number"); + } + if (distDir !== undefined && args.length > 0) { + throw new Error("Pass either or --dist, not both"); + } + if (distDir === undefined && args.length !== 1) { + throw new Error("Missing URL or --dist"); + } + + return { + browserName, + distDir, + profileName, + settleMs, + timeoutSeconds, + url: args[0], + }; +} + +function isWithinRoot(root, filePath) { + const relativePath = relative(root, filePath); + return ( + relativePath.length === 0 || + (!relativePath.startsWith("..") && !relativePath.startsWith(sep)) + ); +} + +function acceptsEncoding(acceptEncodingHeader, encoding) { + if (acceptEncodingHeader === undefined) { + return false; + } + return acceptEncodingHeader.split(",").some((part) => { + const [rawName, ...rawParams] = part.trim().split(";"); + const name = rawName?.trim().toLowerCase(); + if (name !== encoding && name !== "*") { + return false; + } + const qParam = rawParams + .map((param) => param.trim().toLowerCase()) + .find((param) => param.startsWith("q=")); + if (qParam === undefined) { + return true; + } + const quality = Number(qParam.slice(2)); + return Number.isNaN(quality) || quality > 0; + }); +} + +function findPrecompressedFile(filePath, acceptEncodingHeader) { + for (const candidate of PRECOMPRESSED) { + if (!acceptsEncoding(acceptEncodingHeader, candidate.encoding)) { + continue; + } + const encodedFilePath = `${filePath}${candidate.extension}`; + if (existsSync(encodedFilePath) && statSync(encodedFilePath).isFile()) { + return { + encoding: candidate.encoding, + filePath: encodedFilePath, + }; + } + } + return null; +} + +function createStaticDistServer(distDir) { + const root = resolve(distDir); + if (!existsSync(resolve(root, "index.html"))) { + throw new Error(`Missing ${resolve(root, "index.html")}`); + } + + const server = createServer((request, response) => { + const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1"); + if (requestUrl.pathname.startsWith("/api/")) { + response.writeHead(404, { "content-type": "application/json" }); + response.end(JSON.stringify({ code: "not_found" })); + return; + } + + const urlPath = + requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname; + let filePath = resolve(root, `.${decodeURIComponent(urlPath)}`); + if (!isWithinRoot(root, filePath) || !existsSync(filePath)) { + filePath = resolve(root, "index.html"); + } + if (!statSync(filePath).isFile()) { + response.writeHead(404); + response.end(); + return; + } + + const contentType = MIME[extname(filePath)] ?? "application/octet-stream"; + const cacheControl = urlPath.startsWith("/assets/") + ? "public, max-age=31536000, immutable" + : "no-store"; + const precompressed = + contentType !== "text/html" + ? findPrecompressedFile(filePath, request.headers["accept-encoding"]) + : null; + const servedPath = precompressed?.filePath ?? filePath; + const body = readFileSync(servedPath); + const headers = { + "cache-control": cacheControl, + "content-length": String(body.length), + "content-type": contentType, + }; + if (precompressed !== null) { + headers["content-encoding"] = precompressed.encoding; + headers.vary = "Accept-Encoding"; + } + + response.writeHead(200, headers); + response.end(body); + }); + + return new Promise((resolvePromise) => { + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Could not resolve static server address"); + } + resolvePromise({ + close: () => + new Promise((resolveClose, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolveClose(); + }); + }), + url: `http://127.0.0.1:${address.port}/`, + }); + }); + }); +} + +function createDevBrowserScript(config) { + return ` +const config = ${JSON.stringify(config)}; +const page = await browser.newPage(); +const responses = []; + +page.on("response", (response) => { + try { + const headers = response.headers(); + responses.push({ + contentEncoding: headers["content-encoding"] || null, + contentLength: headers["content-length"] || null, + cacheControl: headers["cache-control"] || null, + resourceType: response.request().resourceType(), + status: response.status(), + url: response.url(), + }); + } catch (_error) { + // Keep measurement best-effort; resource timing still captures the request. + } +}); + +let throttleApplied = false; +if (config.profile !== null) { + try { + const client = await page.context().newCDPSession(page); + await client.send("Network.enable"); + await client.send("Network.setCacheDisabled", { cacheDisabled: true }); + await client.send("Network.emulateNetworkConditions", { + offline: false, + latency: config.profile.latencyMs, + downloadThroughput: config.profile.downloadKibPerSecond * 1024, + uploadThroughput: config.profile.uploadKibPerSecond * 1024, + }); + throttleApplied = true; + } catch (error) { + console.warn("Could not apply CDP network profile:", String(error)); + } +} + +const startedAt = Date.now(); +let navigationError = null; +try { + await page.goto(config.url, { + waitUntil: "domcontentloaded", + timeout: config.navigationTimeoutMs, + }); +} catch (error) { + navigationError = String(error); +} +await page.waitForTimeout(config.settleMs); + +const pageMetrics = await page.evaluate(() => { + const navigation = performance.getEntriesByType("navigation")[0]; + const paints = performance.getEntriesByType("paint").map((entry) => ({ + name: entry.name, + startTime: entry.startTime, + })); + const resources = performance.getEntriesByType("resource").map((entry) => ({ + decodedBodySize: entry.decodedBodySize, + duration: entry.duration, + encodedBodySize: entry.encodedBodySize, + initiatorType: entry.initiatorType, + name: entry.name, + startTime: entry.startTime, + transferSize: entry.transferSize, + })); + return { + bodyText: document.body?.innerText?.slice(0, 240) ?? "", + navigation: navigation ? { + decodedBodySize: navigation.decodedBodySize, + domContentLoadedEventEnd: navigation.domContentLoadedEventEnd, + duration: navigation.duration, + encodedBodySize: navigation.encodedBodySize, + loadEventEnd: navigation.loadEventEnd, + responseEnd: navigation.responseEnd, + transferSize: navigation.transferSize, + } : null, + paints, + readyState: document.readyState, + resources, + title: document.title, + url: location.href, + }; +}); + +const navigation = pageMetrics.navigation; +const resourceTotals = pageMetrics.resources.reduce((total, resource) => ({ + decodedBodySize: total.decodedBodySize + resource.decodedBodySize, + encodedBodySize: total.encodedBodySize + resource.encodedBodySize, + transferSize: total.transferSize + resource.transferSize, +}), { decodedBodySize: 0, encodedBodySize: 0, transferSize: 0 }); +const totals = { + decodedBodySize: resourceTotals.decodedBodySize + (navigation?.decodedBodySize ?? 0), + encodedBodySize: resourceTotals.encodedBodySize + (navigation?.encodedBodySize ?? 0), + transferSize: resourceTotals.transferSize + (navigation?.transferSize ?? 0), +}; +const contentEncodings = responses.reduce((counts, response) => { + const key = response.contentEncoding ?? "identity"; + counts[key] = (counts[key] ?? 0) + 1; + return counts; +}, {}); +const largestResources = pageMetrics.resources + .slice() + .sort((a, b) => b.decodedBodySize - a.decodedBodySize) + .slice(0, 12); + +console.log(JSON.stringify({ + contentEncodings, + elapsedMs: Date.now() - startedAt, + largestResources, + navigation, + navigationError, + paints: pageMetrics.paints, + profileName: config.profileName, + readyState: pageMetrics.readyState, + requestCount: responses.length, + throttleApplied, + title: pageMetrics.title, + totals, + url: pageMetrics.url, +}, null, 2)); + +await page.close(); +`; +} + +function runDevBrowser(command, args, input) { + return new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + child.stdout.on("data", (chunk) => { + process.stdout.write(chunk); + }); + child.stderr.on("data", (chunk) => { + process.stderr.write(chunk); + }); + child.on("error", reject); + child.on("close", (code) => { + resolvePromise(code ?? 1); + }); + child.stdin.end(input); + }); +} + +async function main() { + const options = parseArgs(); + let localServer; + const url = + options.distDir === undefined + ? options.url + : (localServer = await createStaticDistServer(options.distDir)).url; + const devBrowserBin = process.env.DEV_BROWSER_BIN; + const command = devBrowserBin ?? "npx"; + const devBrowserArgs = + devBrowserBin === undefined ? ["-y", DEV_BROWSER_PACKAGE] : []; + devBrowserArgs.push( + "--headless", + "--browser", + options.browserName, + "--timeout", + String(options.timeoutSeconds), + ); + + try { + const exitCode = await runDevBrowser( + command, + devBrowserArgs, + createDevBrowserScript({ + navigationTimeoutMs: options.timeoutSeconds * 1000, + profile: PROFILES[options.profileName], + profileName: options.profileName, + settleMs: options.settleMs, + url, + }), + ); + if (exitCode !== 0) { + process.exit(exitCode); + } + } finally { + if (localServer !== undefined) { + await localServer.close(); + } + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/precompress-app-dist.mjs b/scripts/precompress-app-dist.mjs new file mode 100644 index 000000000..68709d3f3 --- /dev/null +++ b/scripts/precompress-app-dist.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +import { + constants as zlibConstants, + brotliCompressSync, + gzipSync, +} from "node:zlib"; +import { + existsSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import { extname, join, resolve } from "node:path"; + +const DEFAULT_DIST_DIR = "apps/app/dist"; +const MIN_COMPRESS_BYTES = 1024; +const COMPRESSIBLE_EXTENSIONS = new Set([ + ".css", + ".js", + ".json", + ".mjs", + ".svg", + ".txt", + ".wasm", + ".webmanifest", + ".xml", +]); + +function usage() { + console.error("Usage: node scripts/precompress-app-dist.mjs [dist-dir]"); +} + +function walkFiles(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const filePath = join(dir, entry.name); + return entry.isDirectory() ? walkFiles(filePath) : [filePath]; + }); +} + +function shouldPrecompress(filePath) { + if (filePath.endsWith(".br") || filePath.endsWith(".gz")) { + return false; + } + return COMPRESSIBLE_EXTENSIONS.has(extname(filePath)); +} + +function writeIfSmaller(args) { + if (args.compressed.length >= args.rawLength) { + return false; + } + writeFileSync(args.outputPath, args.compressed); + return true; +} + +const args = process.argv.slice(2); +if (args.includes("-h") || args.includes("--help")) { + usage(); + process.exit(0); +} +if (args.length > 1) { + usage(); + process.exit(1); +} + +const distDir = resolve(args[0] ?? DEFAULT_DIST_DIR); +if (!existsSync(distDir)) { + console.error( + `Missing ${distDir}. Run: pnpm exec turbo run build --filter=@bb/app`, + ); + process.exit(1); +} + +let sourceFiles = 0; +let brotliFiles = 0; +let gzipFiles = 0; + +for (const filePath of walkFiles(distDir)) { + if (!shouldPrecompress(filePath)) { + continue; + } + const fileStat = statSync(filePath); + if (!fileStat.isFile() || fileStat.size < MIN_COMPRESS_BYTES) { + continue; + } + + sourceFiles += 1; + const body = readFileSync(filePath); + const brotli = brotliCompressSync(body, { + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: 10, + }, + }); + const gzip = gzipSync(body, { level: 9 }); + + if ( + writeIfSmaller({ + compressed: brotli, + outputPath: `${filePath}.br`, + rawLength: body.length, + }) + ) { + brotliFiles += 1; + } + if ( + writeIfSmaller({ + compressed: gzip, + outputPath: `${filePath}.gz`, + rawLength: body.length, + }) + ) { + gzipFiles += 1; + } +} + +console.log( + `precompressed ${sourceFiles} files (${brotliFiles} br, ${gzipFiles} gzip) in ${distDir}`, +); From 3d244cfa052b15bfd18f162539edf098a4839b82 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Sun, 28 Jun 2026 19:22:54 -0700 Subject: [PATCH 5/5] Remove startup measurement scripts --- scripts/measure-app-startup-payload.mjs | 138 -------- scripts/measure-browser-startup.mjs | 414 ------------------------ 2 files changed, 552 deletions(-) delete mode 100644 scripts/measure-app-startup-payload.mjs delete mode 100644 scripts/measure-browser-startup.mjs diff --git a/scripts/measure-app-startup-payload.mjs b/scripts/measure-app-startup-payload.mjs deleted file mode 100644 index c5d827ff3..000000000 --- a/scripts/measure-app-startup-payload.mjs +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env node -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; -import { join, resolve } from "node:path"; -import { - constants as zlibConstants, - brotliCompressSync, - gzipSync, -} from "node:zlib"; - -const DEFAULT_DIST_DIR = "apps/app/dist"; - -function usage() { - console.error( - "Usage: node scripts/measure-app-startup-payload.mjs [dist-dir]", - ); -} - -function formatBytes(bytes) { - if (bytes < 1024) return `${bytes} B`; - const kib = bytes / 1024; - if (kib < 1024) return `${kib.toFixed(1)} KiB`; - return `${(kib / 1024).toFixed(2)} MiB`; -} - -function walkFiles(dir) { - return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - const filePath = join(dir, entry.name); - return entry.isDirectory() ? walkFiles(filePath) : [filePath]; - }); -} - -function measureFile(filePath, label) { - const body = readFileSync(filePath); - return { - brotliBytes: brotliCompressSync(body, { - params: { - [zlibConstants.BROTLI_PARAM_QUALITY]: 10, - }, - }).length, - gzipBytes: gzipSync(body).length, - label, - rawBytes: body.length, - }; -} - -function extractInitialReferences(html) { - const references = []; - const pattern = /<(script|link)\b[^>]+(?:src|href)="([^"]+)"/g; - for (const match of html.matchAll(pattern)) { - const url = match[2]; - if (url.startsWith("/")) { - references.push(url); - } - } - return references; -} - -function sum(items, key) { - return items.reduce((total, item) => total + item[key], 0); -} - -function printTable(title, items) { - console.log(`\n${title}`); - for (const item of items) { - console.log( - `${formatBytes(item.rawBytes).padStart(10)} raw ${formatBytes( - item.gzipBytes, - ).padStart(10)} gzip ${formatBytes(item.brotliBytes).padStart( - 10, - )} br ${item.label}`, - ); - } -} - -const args = process.argv.slice(2); -if (args.includes("-h") || args.includes("--help")) { - usage(); - process.exit(0); -} -if (args.length > 1) { - usage(); - process.exit(1); -} - -const distDir = resolve(args[0] ?? DEFAULT_DIST_DIR); -const indexPath = join(distDir, "index.html"); -if (!existsSync(indexPath)) { - console.error( - `Missing ${indexPath}. Run: pnpm exec turbo run build --filter=@bb/app`, - ); - process.exit(1); -} - -const indexHtml = readFileSync(indexPath, "utf8"); -const initialReferences = extractInitialReferences(indexHtml); -const initialFiles = [ - measureFile(indexPath, "/index.html"), - ...initialReferences - .map((reference) => { - const filePath = join(distDir, reference.slice(1)); - return existsSync(filePath) && statSync(filePath).isFile() - ? measureFile(filePath, reference) - : null; - }) - .filter(Boolean), -]; -const initialRequestCount = initialFiles.length; -const allFiles = walkFiles(distDir) - .filter( - (filePath) => - !filePath.endsWith(".br") && - !filePath.endsWith(".gz") && - !filePath.endsWith(".map"), - ) - .map((filePath) => - measureFile(filePath, `/${filePath.slice(distDir.length + 1)}`), - ) - .sort((a, b) => b.rawBytes - a.rawBytes); - -console.log(`dist: ${distDir}`); -console.log(`initial references: ${initialReferences.length}`); -console.log(`initial requests: ${initialRequestCount}`); -console.log( - `initial total: ${formatBytes(sum(initialFiles, "rawBytes"))} raw, ${formatBytes( - sum(initialFiles, "gzipBytes"), - )} gzip, ${formatBytes(sum(initialFiles, "brotliBytes"))} br`, -); -console.log( - `all non-map files: ${formatBytes(sum(allFiles, "rawBytes"))} raw, ${formatBytes( - sum(allFiles, "gzipBytes"), - )} gzip, ${formatBytes(sum(allFiles, "brotliBytes"))} br`, -); - -printTable( - "Largest initial files", - [...initialFiles].sort((a, b) => b.rawBytes - a.rawBytes).slice(0, 20), -); -printTable("Largest app files", allFiles.slice(0, 20)); diff --git a/scripts/measure-browser-startup.mjs b/scripts/measure-browser-startup.mjs deleted file mode 100644 index fb2053a80..000000000 --- a/scripts/measure-browser-startup.mjs +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env node -import { createServer } from "node:http"; -import { existsSync, readFileSync, statSync } from "node:fs"; -import { extname, relative, resolve, sep } from "node:path"; -import { spawn } from "node:child_process"; - -const DEV_BROWSER_PACKAGE = "dev-browser@0.2.8"; -const DEFAULT_BROWSER_NAME = "bb-startup-measure"; -const DEFAULT_TIMEOUT_SECONDS = 90; -const DEFAULT_SETTLE_MS = 2_000; -const PROFILES = { - none: null, - airplane: { - downloadKibPerSecond: 80, - latencyMs: 600, - uploadKibPerSecond: 30, - }, -}; -const MIME = { - ".css": "text/css", - ".html": "text/html", - ".ico": "image/x-icon", - ".js": "application/javascript", - ".json": "application/json", - ".png": "image/png", - ".svg": "image/svg+xml", - ".webmanifest": "application/manifest+json", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", -}; -const PRECOMPRESSED = [ - { encoding: "br", extension: ".br" }, - { encoding: "gzip", extension: ".gz" }, -]; - -function usage() { - console.error(`Usage: - node scripts/measure-browser-startup.mjs [--profile none|airplane] - node scripts/measure-browser-startup.mjs --dist apps/app/dist [--profile none|airplane] - -Options: - --browser-name dev-browser instance name (default: ${DEFAULT_BROWSER_NAME}) - --settle-ms wait after DOMContentLoaded before collecting metrics - --timeout dev-browser script timeout (default: ${DEFAULT_TIMEOUT_SECONDS}) - -By default this runs: npx -y ${DEV_BROWSER_PACKAGE} --headless -Set DEV_BROWSER_BIN=dev-browser to use an installed binary instead.`); -} - -function takeOption(args, name) { - const index = args.indexOf(name); - if (index === -1) { - return undefined; - } - const value = args[index + 1]; - if (value === undefined || value.startsWith("--")) { - throw new Error(`Missing value for ${name}`); - } - args.splice(index, 2); - return value; -} - -function parseArgs() { - const args = process.argv.slice(2); - if (args.includes("-h") || args.includes("--help")) { - usage(); - process.exit(0); - } - - const browserName = - takeOption(args, "--browser-name") ?? DEFAULT_BROWSER_NAME; - const distDir = takeOption(args, "--dist"); - const profileName = takeOption(args, "--profile") ?? "none"; - const settleMs = Number(takeOption(args, "--settle-ms") ?? DEFAULT_SETTLE_MS); - const timeoutSeconds = Number( - takeOption(args, "--timeout") ?? DEFAULT_TIMEOUT_SECONDS, - ); - - if (!(profileName in PROFILES)) { - throw new Error(`Unknown profile "${profileName}"`); - } - if (!Number.isFinite(settleMs) || settleMs < 0) { - throw new Error("--settle-ms must be a non-negative number"); - } - if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) { - throw new Error("--timeout must be a positive number"); - } - if (distDir !== undefined && args.length > 0) { - throw new Error("Pass either or --dist, not both"); - } - if (distDir === undefined && args.length !== 1) { - throw new Error("Missing URL or --dist"); - } - - return { - browserName, - distDir, - profileName, - settleMs, - timeoutSeconds, - url: args[0], - }; -} - -function isWithinRoot(root, filePath) { - const relativePath = relative(root, filePath); - return ( - relativePath.length === 0 || - (!relativePath.startsWith("..") && !relativePath.startsWith(sep)) - ); -} - -function acceptsEncoding(acceptEncodingHeader, encoding) { - if (acceptEncodingHeader === undefined) { - return false; - } - return acceptEncodingHeader.split(",").some((part) => { - const [rawName, ...rawParams] = part.trim().split(";"); - const name = rawName?.trim().toLowerCase(); - if (name !== encoding && name !== "*") { - return false; - } - const qParam = rawParams - .map((param) => param.trim().toLowerCase()) - .find((param) => param.startsWith("q=")); - if (qParam === undefined) { - return true; - } - const quality = Number(qParam.slice(2)); - return Number.isNaN(quality) || quality > 0; - }); -} - -function findPrecompressedFile(filePath, acceptEncodingHeader) { - for (const candidate of PRECOMPRESSED) { - if (!acceptsEncoding(acceptEncodingHeader, candidate.encoding)) { - continue; - } - const encodedFilePath = `${filePath}${candidate.extension}`; - if (existsSync(encodedFilePath) && statSync(encodedFilePath).isFile()) { - return { - encoding: candidate.encoding, - filePath: encodedFilePath, - }; - } - } - return null; -} - -function createStaticDistServer(distDir) { - const root = resolve(distDir); - if (!existsSync(resolve(root, "index.html"))) { - throw new Error(`Missing ${resolve(root, "index.html")}`); - } - - const server = createServer((request, response) => { - const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1"); - if (requestUrl.pathname.startsWith("/api/")) { - response.writeHead(404, { "content-type": "application/json" }); - response.end(JSON.stringify({ code: "not_found" })); - return; - } - - const urlPath = - requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname; - let filePath = resolve(root, `.${decodeURIComponent(urlPath)}`); - if (!isWithinRoot(root, filePath) || !existsSync(filePath)) { - filePath = resolve(root, "index.html"); - } - if (!statSync(filePath).isFile()) { - response.writeHead(404); - response.end(); - return; - } - - const contentType = MIME[extname(filePath)] ?? "application/octet-stream"; - const cacheControl = urlPath.startsWith("/assets/") - ? "public, max-age=31536000, immutable" - : "no-store"; - const precompressed = - contentType !== "text/html" - ? findPrecompressedFile(filePath, request.headers["accept-encoding"]) - : null; - const servedPath = precompressed?.filePath ?? filePath; - const body = readFileSync(servedPath); - const headers = { - "cache-control": cacheControl, - "content-length": String(body.length), - "content-type": contentType, - }; - if (precompressed !== null) { - headers["content-encoding"] = precompressed.encoding; - headers.vary = "Accept-Encoding"; - } - - response.writeHead(200, headers); - response.end(body); - }); - - return new Promise((resolvePromise) => { - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (address === null || typeof address === "string") { - throw new Error("Could not resolve static server address"); - } - resolvePromise({ - close: () => - new Promise((resolveClose, reject) => { - server.close((error) => { - if (error) { - reject(error); - return; - } - resolveClose(); - }); - }), - url: `http://127.0.0.1:${address.port}/`, - }); - }); - }); -} - -function createDevBrowserScript(config) { - return ` -const config = ${JSON.stringify(config)}; -const page = await browser.newPage(); -const responses = []; - -page.on("response", (response) => { - try { - const headers = response.headers(); - responses.push({ - contentEncoding: headers["content-encoding"] || null, - contentLength: headers["content-length"] || null, - cacheControl: headers["cache-control"] || null, - resourceType: response.request().resourceType(), - status: response.status(), - url: response.url(), - }); - } catch (_error) { - // Keep measurement best-effort; resource timing still captures the request. - } -}); - -let throttleApplied = false; -if (config.profile !== null) { - try { - const client = await page.context().newCDPSession(page); - await client.send("Network.enable"); - await client.send("Network.setCacheDisabled", { cacheDisabled: true }); - await client.send("Network.emulateNetworkConditions", { - offline: false, - latency: config.profile.latencyMs, - downloadThroughput: config.profile.downloadKibPerSecond * 1024, - uploadThroughput: config.profile.uploadKibPerSecond * 1024, - }); - throttleApplied = true; - } catch (error) { - console.warn("Could not apply CDP network profile:", String(error)); - } -} - -const startedAt = Date.now(); -let navigationError = null; -try { - await page.goto(config.url, { - waitUntil: "domcontentloaded", - timeout: config.navigationTimeoutMs, - }); -} catch (error) { - navigationError = String(error); -} -await page.waitForTimeout(config.settleMs); - -const pageMetrics = await page.evaluate(() => { - const navigation = performance.getEntriesByType("navigation")[0]; - const paints = performance.getEntriesByType("paint").map((entry) => ({ - name: entry.name, - startTime: entry.startTime, - })); - const resources = performance.getEntriesByType("resource").map((entry) => ({ - decodedBodySize: entry.decodedBodySize, - duration: entry.duration, - encodedBodySize: entry.encodedBodySize, - initiatorType: entry.initiatorType, - name: entry.name, - startTime: entry.startTime, - transferSize: entry.transferSize, - })); - return { - bodyText: document.body?.innerText?.slice(0, 240) ?? "", - navigation: navigation ? { - decodedBodySize: navigation.decodedBodySize, - domContentLoadedEventEnd: navigation.domContentLoadedEventEnd, - duration: navigation.duration, - encodedBodySize: navigation.encodedBodySize, - loadEventEnd: navigation.loadEventEnd, - responseEnd: navigation.responseEnd, - transferSize: navigation.transferSize, - } : null, - paints, - readyState: document.readyState, - resources, - title: document.title, - url: location.href, - }; -}); - -const navigation = pageMetrics.navigation; -const resourceTotals = pageMetrics.resources.reduce((total, resource) => ({ - decodedBodySize: total.decodedBodySize + resource.decodedBodySize, - encodedBodySize: total.encodedBodySize + resource.encodedBodySize, - transferSize: total.transferSize + resource.transferSize, -}), { decodedBodySize: 0, encodedBodySize: 0, transferSize: 0 }); -const totals = { - decodedBodySize: resourceTotals.decodedBodySize + (navigation?.decodedBodySize ?? 0), - encodedBodySize: resourceTotals.encodedBodySize + (navigation?.encodedBodySize ?? 0), - transferSize: resourceTotals.transferSize + (navigation?.transferSize ?? 0), -}; -const contentEncodings = responses.reduce((counts, response) => { - const key = response.contentEncoding ?? "identity"; - counts[key] = (counts[key] ?? 0) + 1; - return counts; -}, {}); -const largestResources = pageMetrics.resources - .slice() - .sort((a, b) => b.decodedBodySize - a.decodedBodySize) - .slice(0, 12); - -console.log(JSON.stringify({ - contentEncodings, - elapsedMs: Date.now() - startedAt, - largestResources, - navigation, - navigationError, - paints: pageMetrics.paints, - profileName: config.profileName, - readyState: pageMetrics.readyState, - requestCount: responses.length, - throttleApplied, - title: pageMetrics.title, - totals, - url: pageMetrics.url, -}, null, 2)); - -await page.close(); -`; -} - -function runDevBrowser(command, args, input) { - return new Promise((resolvePromise, reject) => { - const child = spawn(command, args, { - stdio: ["pipe", "pipe", "pipe"], - }); - child.stdout.on("data", (chunk) => { - process.stdout.write(chunk); - }); - child.stderr.on("data", (chunk) => { - process.stderr.write(chunk); - }); - child.on("error", reject); - child.on("close", (code) => { - resolvePromise(code ?? 1); - }); - child.stdin.end(input); - }); -} - -async function main() { - const options = parseArgs(); - let localServer; - const url = - options.distDir === undefined - ? options.url - : (localServer = await createStaticDistServer(options.distDir)).url; - const devBrowserBin = process.env.DEV_BROWSER_BIN; - const command = devBrowserBin ?? "npx"; - const devBrowserArgs = - devBrowserBin === undefined ? ["-y", DEV_BROWSER_PACKAGE] : []; - devBrowserArgs.push( - "--headless", - "--browser", - options.browserName, - "--timeout", - String(options.timeoutSeconds), - ); - - try { - const exitCode = await runDevBrowser( - command, - devBrowserArgs, - createDevBrowserScript({ - navigationTimeoutMs: options.timeoutSeconds * 1000, - profile: PROFILES[options.profileName], - profileName: options.profileName, - settleMs: options.settleMs, - url, - }), - ); - if (exitCode !== 0) { - process.exit(exitCode); - } - } finally { - if (localServer !== undefined) { - await localServer.close(); - } - } -} - -main().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); -});