From 61d9faffc51c508017204ba85a3f82a5036c721f Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 9 Aug 2026 03:20:28 +0200 Subject: [PATCH] perf: keep the API-docs SPA and MCP SDK out of the server graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @scalar/api-reference-react is a documentation SPA reachable only from one admin tab, and it renders nothing usable on the server, yet it was compiled into the server graph for an SSR pass whose output is discarded. It is a client component already, so next/dynamic with ssr: false is allowed in place and needs no restructuring. @modelcontextprotocol/sdk is only reachable through /api/mcp/[transport], so it moves to serverExternalPackages. Honest accounting: this is an image-size and architecture change, not a measured memory win. Idle landed at 89.4 MiB against 90.3/91.1/100.5 for the previous build — the better end of that spread, but inside it, so on one sample it cannot be claimed as a reduction. What is deterministic is image size, 401 -> 398 MB. Kept because a docs SPA should not be server-rendered regardless of what the memory number does. pnpm typecheck and lint clean; pnpm test 1794/1795 with the known Testcontainers pi-hole flake, which passes 9/9 in isolation. --- PHASE2-FINDINGS.md | 27 +++++++++++++++++++ apps/nextjs/next.config.ts | 2 ++ .../api/components/scalar-api-reference.tsx | 9 ++++++- .../items/widget-hover-overlay.module.css | 6 ++--- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/PHASE2-FINDINGS.md b/PHASE2-FINDINGS.md index 31da9f5c1..779e4fab6 100644 --- a/PHASE2-FINDINGS.md +++ b/PHASE2-FINDINGS.md @@ -128,6 +128,33 @@ a real trade, not a free win. **Cumulative vs merge-base: idle 141.7 → 90.3 MiB (−36%), board loaded 257.8 → 164.6 MiB (−36%), under load 392.7 → 302.5 MiB (−23%).** +## Final A/B, with repetitions + +Everything above is one sample per configuration, which is fine for ranking changes but +not for a headline. This is the merge-base image against the PR's end state, alternated, +on the same backup — 2 runs before, 3 after: + +| run | idle | board loaded | under load | after soak | peak | +| --- | --- | --- | --- | --- | --- | +| before #1 | 144.2 | 249.0 | 392.8 | 529.6 | 753.0 | +| before #2 | 142.3 | 268.2 | 385.1 | 538.5 | 626.1 | +| after #1 | 100.5 | 176.8 | 311.1 | 204.3 | 590.9 | +| after #2 | 91.1 | 163.5 | 319.9 | 212.7 | 568.0 | +| after #3 | 90.3 | 164.6 | 302.5 | 206.4 | 594.2 | + +| metric | before (mean) | after (mean) | change | +| --- | --- | --- | --- | +| boot idle | 143.3 | **94.0** | **−34%** | +| board loaded | 258.6 | **168.3** | **−35%** | +| under load | 389.0 | **311.2** | **−20%** | +| after idle soak | 534.1 | **207.8** | **−61%** | +| peak | 689.6 | **584.4** | −15% | + +The two groups do not overlap on any stage. The before-side idle samples land within +1.9 MiB of each other, which is what justifies treating a ~50 MiB gap as real rather than +noise. Earlier single-sample rounds suggested −36%; the repeated figure is **−34%**, and +that is the number used in the PR. + ## Is the measurement actually behind a login? Worth stating explicitly, because "28 widgets rendered" would be meaningless if the board diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 18b058def..08f79872d 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -51,6 +51,8 @@ const nextConfig: NextConfig = { "node-unifi", "@kubernetes/client-node", "linkedom", + // Only reachable through /api/mcp/[transport]. + "@modelcontextprotocol/sdk", ], experimental: { optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], diff --git a/apps/nextjs/src/app/[locale]/manage/tools/api/components/scalar-api-reference.tsx b/apps/nextjs/src/app/[locale]/manage/tools/api/components/scalar-api-reference.tsx index c4958d125..4707bee2f 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/api/components/scalar-api-reference.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/api/components/scalar-api-reference.tsx @@ -1,11 +1,18 @@ "use client"; +import dynamic from "next/dynamic"; import { useComputedColorScheme } from "@mantine/core"; -import { ApiReferenceReact } from "@scalar/api-reference-react"; import "@scalar/api-reference-react/style.css"; import "./scalar-theme.css"; +// The API reference is a whole documentation SPA reachable only from this one admin +// tab, and it renders nothing useful on the server. Loading it lazily keeps it out +// of the server graph instead of compiling it for an SSR pass that is discarded. +const ApiReferenceReact = dynamic(() => import("@scalar/api-reference-react").then((mod) => mod.ApiReferenceReact), { + ssr: false, +}); + interface ScalarApiReferenceProps { document: object; } diff --git a/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css b/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css index 89b3ba93e..f6ec2600f 100644 --- a/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css +++ b/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css @@ -13,10 +13,8 @@ transform 150ms ease; } -:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover) - .wrapper, -:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within) - .wrapper { +:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover) .wrapper, +:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within) .wrapper { opacity: 1; transform: translateY(0); }