Hackorda Docs
Scale

Frontend Bundle

Captured: 2026-05-15 · next build (Next.js 15.5.11, App Router) · branch claude/scale-4-frontend

This is a documentation deliverable for the 10x-scale push. It records the current next build route/bundle picture and recommends concrete code-splitting work. The splitting itself is backlog — not done in this round.

How this was captured

npm run build compiles cleanly but the prerender step fails locally without Clerk env vars (Missing publishableKey). To get the route table, the build was re-run with throwaway dummy Clerk keys (pk_test_…/sk_test_…) — those keys are only passed inline to the local build process and are not committed. The compile + type-check + chunking output is unaffected by which keys are used.

Shared baseline

First Load JS shared by all          102 kB
  ├ chunks/1255-*.js                  45.7 kB
  ├ chunks/4bd1b696-*.js              54.2 kB
  └ other shared chunks (total)        2.04 kB
Middleware                            98.8 kB

Every route pays the 102 kB shared baseline. The two big shared chunks are the React/Next runtime + Clerk + TanStack Query — unavoidable common ground.

Heaviest routes (route-specific JS / First Load JS)

RouteRoute JSFirst Load JSNotes
/app/admin/test-cycles/new29.9 kB207 kBCycle-creation wizard — heaviest route-specific chunk in the app
/app/admin/triage17.9 kB166 kBCross-cycle triage board
/app/test-cycles/[id]/issues/[issueId]18.5 kB213 kBIssue detail (comments, payout pipeline, markdown)
/app/admin/test-cycles/[id]16.4 kB242 kBAdmin cycle detail — highest First Load JS in /app
/app/profile1.0 kB288 kBTiny route code but pulls a huge shared/profile graph
/app/admin/users/[id]0.75 kB258 kBSame pattern — small page, heavy imported graph
/app/admin/events/[id]/edit2.7 kB230 kBdnd-kit + form stack
/app/account5.0 kB228 kB
/app/test-cycles/[id]11.5 kB221 kBTester cycle detail
/app/admin/test-cycles/[id]/new (issues/new)10.1 kB191 kBIssue intake form
/app/leaderboard/testers10.7 kB181 kBRecently-merged tester leaderboard

The two cycle detail pages — the focus of the scale push — are 221 kB (tester) and 242 kB (admin) First Load JS. The admin page is the single heaviest /app route by First Load JS.

Findings

1. Admin-only code is NOT leaking into tester bundles ✅

This was the headline concern. Verified:

  • src/lib/tester-stats.ts (~936 lines) is imported only by two API routes (/api/admin/testers/leaderboard, /api/admin/testers/[userId]/stats). No client component imports it, so it never ships to a browser bundle at all — it stays server-side.
  • The tester leaderboard page (/app/leaderboard/testers) is its own route segment. Its 10.7 kB of route code is loaded only when that URL is visited; it does not bleed into the cycle detail pages or tester dashboards.

So there is no admin→tester bundle leak today. The route-based code splitting the App Router gives for free is doing its job at the page boundary.

2. The real waste is within routes, not across them

The cycle detail pages bundle everything eagerly even though most of it is behind a tab the user may never open:

  • Admin /app/admin/test-cycles/[id] (242 kB) eagerly imports, per tab: CycleDocsPanel (Docs), MembersPanel + CyclePayoutByTester (Members), VerificationGatePanel / PayoutRatesPanel / JoinPolicyPanel (Settings), the brief editor dialog, and the Issues triage stack. Only one tab is visible at a time, yet all of it is in the first chunk.
  • Tester /app/test-cycles/[id] (221 kB) has the same shape: CycleDocsPanel, RunPackPanel, MarkdownView, and the Issues list all load up front.

3. recharts is dead weight

src/components/ui/chart.tsx wraps recharts (a heavy charting lib) but no component imports ui/chart anywhere in the app. Tree-shaking should drop it, but the dependency is still installed and a stray future import would silently add a large chunk. Recommend removing recharts + ui/chart.tsx if charts aren't on the roadmap (out of scope for this agent — flagged for cleanup).

4. Two date libraries are bundled

Both date-fns (v4) and dayjs are dependencies. date-fns is used by exactly one client component (DateTimePicker.tsx); dayjs is used broadly across profile/events/teams components. Standardising on one (likely dayjs, the smaller + more-used one) would shave a chunk. Most cycle-detail date formatting already uses the native Date API, so the cycle pages aren't directly affected — this is an app-wide cleanup.

5. markdown.ts is clean

src/lib/markdown.ts is a tiny hand-rolled markdown→HTML renderer with zero imports — it does not pull remark/rehype into client bundles. remark* deps are server-only. No action needed.

Recommendations (BACKLOG — not done this round)

Priority order for a follow-up code-splitting pass:

  1. Lazy-load non-default cycle tabs. Wrap CycleDocsPanel, the Members-tab panels, and the Settings-tab panels in next/dynamic(() => import(…), { ssr: false }) on both cycle detail pages. The Issues tab is the common landing tab and should stay eager; everything else loads on tab activation. Estimated First Load JS reduction: ~40–70 kB on the admin cycle page.
  2. Lazy-load the brief editor dialog (CycleBriefSidePanel / brief editor) — it's only opened on an explicit "Open editor" click, so it has no business in the first chunk.
  3. Code-split /app/admin/test-cycles/new (29.9 kB) — the heaviest route chunk. The multi-step wizard's later steps can be dynamically imported.
  4. Remove recharts + ui/chart.tsx if charting is not planned.
  5. Consolidate date-fnsdayjs (or vice-versa) app-wide; drop the unused one from package.json.
  6. @tanstack/react-virtual was added this round for list virtualization. It is small (~5 kB) and loads only on the cycle detail routes that import VirtualizedBucketList — acceptable, no split needed.

What changed this round (for context)

This branch added @tanstack/react-virtual and VirtualizedBucketList, which virtualizes the issue list on both cycle detail pages past ~100 issues. That reduces DOM node count and render/layout cost at scale (500+ issues) but does not change the JS bundle materially — virtualization is a runtime win, not a bundle-size win. The bundle-size wins above are still open.

On this page