From 1bed15a1889ad66719c64cba0e040db4ff4061fd Mon Sep 17 00:00:00 2001
From: Jim Bethancourt
Date: Wed, 28 Oct 2026 05:59:05 -0500
Subject: [PATCH 1/7] Improving page load performance and applying coderabbit
fixes
---
AGENTS.md | 14 +-
README.md | 3 +-
app/[username]/page.jsx | 6 +
components/landing.jsx | 6 +
tests/e2e/performance.spec.js | 295 ++++++++++++++++++++++--
tests/integration/landing-page.test.jsx | 20 +-
6 files changed, 318 insertions(+), 26 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 26859bd..5d1c3a6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -116,7 +116,9 @@ components/ # client components: report-view, repo-list,
# hero-search, menu-search, menu-toggle, breadcrumbs
# (route trail under the menu), workflow-sample,
# platform-config, sentry-provider,
- # toast-notification (copy feedback live region)
+ # toast-notification (copy feedback live region),
+ # report-warmup (idle-time rendering-library
+ # warming on directory pages)
lib/ # shared logic (client + RSC): routes, fetcher,
# renderer, search, utils, host, rate-limiter,
# cache-manager, error-handler, repo-submission,
@@ -281,7 +283,15 @@ coverage there when introducing new markup patterns.
the report's arrival causes no layout shift. The vizdom bridge
(`public/widgets/vizdom-bridge.js`) serves the wasm from Cache Storage
(versioned cache name) so repeat visits never re-download it regardless
- of the host's HTTP cache headers.
+ of the host's HTTP cache headers. Directory pages (landing + user
+ listing) mount `components/report-warmup.jsx`, which — after window
+ load, at main-thread idle — warms the shared report rendering libraries
+ only (the vizdom bridge module/wasm, Chart.js + svg-pan-zoom no-cors
+ into the HTTP cache, the bundled template), never report data (listings
+ can hold thousands of repositories), so the first report a visitor opens
+ renders from local caches. The landing bundle budget e2e measures
+ through the window load event for this reason: post-load prefetches and
+ warm-up fetches never delay the landing's own first paint.
- Search ``s are injected by table-enhancer — `` is FORBID in
the renderer's sanitization allow-list, so it must never appear in the
mustache template.
diff --git a/README.md b/README.md
index 386cf42..f80a011 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,8 @@ app/ # Next.js App Router (static export): layout
components/ # Client components (search combobox, hero/menu
# search, menu toggle, workflow sample, user
# listing, report view, submission form,
- # platform-config context, sentry provider)
+ # platform-config context, sentry provider,
+ # report warm-up for directory pages)
lib/ # Shared logic consumed by client components and
# RSC alike (routes, fetcher, renderer, search,
# repo-submission, error-handler, rate-limiter,
diff --git a/app/[username]/page.jsx b/app/[username]/page.jsx
index 3d6615e..c5cc004 100644
--- a/app/[username]/page.jsx
+++ b/app/[username]/page.jsx
@@ -9,6 +9,7 @@ import { userStaticParams } from '../../lib/static-params';
import { reposForUser } from '../../lib/utils';
import { isValidGitHubName } from '../../lib/routes';
import RepoList from '../../components/repo-list';
+import ReportWarmup from '../../components/report-warmup';
export const dynamicParams = false;
@@ -30,6 +31,11 @@ export default async function UserPage({ params }) {
return (
Loading…
}>
+ {/* Directory page: warm the shared report rendering libraries during
+ idle time (vizdom wasm, Chart.js, svg-pan-zoom, the template —
+ never report data) so the first report a visitor opens renders
+ from local caches. */}
+
);
}
diff --git a/components/landing.jsx b/components/landing.jsx
index 59a16f8..c74f273 100644
--- a/components/landing.jsx
+++ b/components/landing.jsx
@@ -1,5 +1,6 @@
import Link from 'next/link';
import HeroSearch from './hero-search';
+import ReportWarmup from './report-warmup';
import { buildRepositoryListUrl } from '../lib/routes.js';
export const FEATURED_COUNT = 6;
@@ -30,6 +31,11 @@ export default function Landing({ repositories }) {
+ {/* Directory page: warm the shared report rendering libraries during
+ idle time (vizdom wasm, Chart.js, svg-pan-zoom, the template —
+ never report data) so the first report a visitor opens renders
+ from local caches. Renders nothing; never delays this page. */}
+
>
);
}
diff --git a/tests/e2e/performance.spec.js b/tests/e2e/performance.spec.js
index 8a1b368..4533519 100644
--- a/tests/e2e/performance.spec.js
+++ b/tests/e2e/performance.spec.js
@@ -5,9 +5,20 @@ import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// Performance / bundle-size guardrails (Technical Appendix §10):
-// - landing page transfers <190KB of first-party code (js/css/html),
-// measured gzip-compressed — production static hosts serve compressed
-// bodies and the local test server does not. (The plan target was 150KB;
+// - landing page ships <190KB of its own first-party code (js/css/html),
+// measured gzip-compressed (production static hosts serve compressed
+// bodies and the local test server does not). Landing assets are
+// identified by document provenance, never by arrival time: everything
+// the landing's HTML references (async chunks + CSS) gates load, so it
+// is exactly the code the landing itself ships. Arrival time cannot
+// draw that line — Next's automatic prefetch downloads the
+// linked routes' chunks (first-party .js as well), and once hydration
+// has run those responses can complete before the window load event;
+// the delayed-logo test below reproduces that race and verifies the
+// exclusion. The idle-time rendering-library warm-up
+// (components/report-warmup.jsx) is excluded the same way: it fetches
+// modules no landing document references.
+// (The plan target was 150KB;
// baselines are recorded as accepted deviations in
// plans/nextjs-conversion.md §10: ~173KB measured at the Next.js
// cutover, re-measured ~185KB after later framework bumps — ~83% of
@@ -21,28 +32,132 @@ import { dirname, join } from 'node:path';
const FIRST_PARTY = url => url.startsWith('http://localhost') && /\.(js|css|html?)(\?|$)/.test(url);
-test('landing page ships less than 190KB of first-party code (gzipped)', async ({ page }) => {
- const sizePromises = [];
- page.on('response', response => {
- if (FIRST_PARTY(response.url())) {
- sizePromises.push((async () => {
- try {
- const body = await response.body();
- return { url: response.url(), bytes: gzipSync(body).length };
- } catch {
- return null; // interrupted navigation
- }
- })());
+/**
+ * Measures the landing's own gzipped payload. Landing assets are
+ * identified by document provenance, never by arrival time: the initial
+ * HTML references exactly the code the landing ships (async chunks +
+ * CSS), and all of it gates load. Arrival time cannot draw that line —
+ * Next's automatic prefetch downloads the linked routes' chunks
+ * (first-party .js as well), and once hydration has run those responses
+ * can complete before the window load event (the delayed-logo test
+ * below reproduces this and verifies the exclusion). The idle-time
+ * rendering-library warm-up (components/report-warmup.jsx) stays out
+ * the same way: it fetches modules no landing document references.
+ *
+ * Returns per-row data for every first-party response:
+ * - landing: { url, bytes } rows for the landing's own assets (the
+ * ones the initial HTML references),
+ * - total: their gzipped byte sum,
+ * - prefetchBeforeLoad: other routes' bytes that completed before the
+ * load event — exactly what an arrival-time listener miscounts into
+ * the budget.
+ *
+ * The optional onForeignFirstParty(url) callback fires once another
+ * route's first-party bytes have downloaded, letting a caller stall the
+ * load event until the miscount window is guaranteed open.
+ */
+async function measureLandingPayload(page, { onForeignFirstParty = null } = {}) {
+ const documentResponse = new Promise(resolve => {
+ page.on('response', response => {
+ if (response.request().resourceType() === 'document') resolve(response);
+ });
+ });
+ const landingPaths = documentResponse.then(async response => {
+ const html = await response.text();
+ const paths = new Set();
+ for (const match of html.matchAll(/