Skip to content

Cut first-load JavaScript: make SiteHeader a server component with a client island #41

Open

Description

@abrcrmb

Task

SiteHeader is a client component mounted in the root layout, so it and its dependencies ship on every route, including the ones a first-time visitor hits on a slow connection. Convert it to a server component with the smallest possible client island, and record the before/after first-load JavaScript.

Context

  • components/nav/SiteHeader.tsx:1 is "use client" and uses usePathname — the only genuinely client part is current-route highlighting.
  • app/layout.tsx:21 mounts it inside the root layout, so it is in the initial payload of every page.
  • Everything else in SiteHeader is static markup plus the mobile menu toggle (:96).
  • app/globals.css:48,78 and motion-reduce: classes on the header must keep working.
  • Lazily loading is already the pattern where it matters: the wallet SDK is dynamically imported (lib/wallet/stellar-wallets-kit.ts:13-15). next/image is absent — there are no raster images, only app/icon.svg and app/apple-icon.png. No next/dynamic, React.lazy or Suspense exists in app/ or components/.
  • app/(marketing)/page.tsx is the landing page a sender sees first; fonts are one Outfit next/font/google with display: "swap" (app/layout.tsx:1,7).

Before you start: mandatory context review

Kinlock moves other people's money, so the docs are binding constraints, not background reading.
They record decisions this task must not silently reopen. Read them in this order and tick each box:

  • docs/ARCHITECTURE_ESSENTIALS.md — start here, it is short (AGENTS.md §1)
  • docs/ARCHITECTURE.md — only the sections named in Context above
  • docs/PRD.md — if this task touches user-facing behaviour or scope
  • AGENTS.md §3 (hard rules), §4 (deferred list), §8 for this repo, §12 (roadmap rule)
  • ROADMAP.md — the row(s) listed under Related roadmap rows, including their Depends on

In your first PR comment, name the hard-rule numbers or doc sections that constrain this task. That
line is how we know the review happened.

Then:

  • If code and docs disagree, or two docs disagree: stop and say so on this issue. Do not pick one
    silently, and do not "fix" either side without approval (AGENTS.md §1).
  • If the task looks like it needs anything from the deferred list (§4), a change to a hard rule (§3),
    or a new dependency (§2): raise it here first. Those need a human; deciding them is not your job.
  • If a row you are told to finish is BLOCKED or depends on a row that is not DONE, say so here
    instead of starting.

Why we need this

  • AGENTS.md §8.3: "mobile-first, usable on slow connections". M3-18's bar is a dashboard load under 2s on a mid-range phone profile.
  • Every KB of client JS on the root layout is paid by every visitor on every route, including someone who arrived from a WhatsApp link on 3G.
  • Client-component boundaries are cheap to fix early and expensive to retrofit once pages depend on the header being a client tree.

Scope

In scope

  • components/nav/SiteHeader.tsx and, if needed, a new components/nav/NavLinks.tsx client island
  • app/layout.tsx (the mount, and no more)
  • A short docs/PERFORMANCE.md note in this repo recording the measured before/after

Do not touch (security-sensitive or generated; changing these needs a human first — AGENTS.md §2, docs/project_structure.md §8)

  • app/claim/** and lib/claim-links/** — the claim-link fragment (#r=…&s=…) must never reach a server, log or analytics tool (hard rule 5)
  • next.config.ts, proxy.ts / middleware.ts, CSP headers
  • lib/wallet/** and any direct contract call site — money-moving calls go through the SDK and the wallet adapter only
  • app/send/**, app/locks/**, app/payee/** action logic — release, refund and decline paths stay with maintainers
  • package.json, pnpm-lock.yaml (no new dependencies without approval), .github/workflows/**
  • adding compression, CDN, SSR caching or analytics configuration — infra and CI are maintainer-owned
  • next.config.ts — flag on the issue if you think a setting belongs there

Suggested implementation

  1. Measure before you change anything: pnpm build and read the per-route First Load JS from the build output, or use Lighthouse on pnpm dev/pnpm start with a throttled profile. Record the numbers.
  2. Split SiteHeader: keep layout and markup server-side, extract only what needs usePathname (and the menu open state) into a small client component. Do not move the whole header into a client boundary by another name.
  3. Keep the reduced-motion and aria-label behaviour intact; the contrast and focus work in this repo's other open issues may touch the same file, so say on the issue which one you picked up first.
  4. Re-measure the same way and put both numbers in the PR and in docs/PERFORMANCE.md. A performance claim without a measurement is not a claim.
  5. If the win is under about 5 KB, say so honestly and we will decide whether it is worth the split. Negative results are useful results.

A different approach is usually welcome — post it here before building and we will answer. This is a
suggestion, not a spec.

Edge cases and traps

  • Measure first, with the method named in the PR (build-output First Load JS or a throttled Lighthouse run). Mixed methods are how a perf claim becomes unverifiable.
  • Only usePathname and the menu open state genuinely need the client; every extra hook you pull across the boundary cancels the win.
  • motion-reduce: and the aria-label on the hamburger must survive the refactor — the keyboard and contrast issues in this repo touch the same file.
  • If the win is under roughly 5 KB, say so. A negative result is a useful result and we will decide the trade-off together.

Done means

  • Before/after First Load JS per route is recorded in the PR and in docs/PERFORMANCE.md
  • The root layout renders SiteHeader as a server component; only the route-aware piece is client
  • Menu, aria-label, focus and reduced-motion all still work; pnpm e2e-style manual check described in the PR
  • No behaviour, copy or route change sneaks into the diff
  • pnpm lint, typecheck, test, build pass

Tests

No new dependency, no new test framework. If you add a build-size assertion, propose it on the issue first — that is a CI-adjacent change.

Documentation

docs/PERFORMANCE.md (new, this repo) with the numbers and how you got them. No ADR unless the component-boundary convention becomes an architectural rule.

Verify before you open the PR

pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm dev

Run every command above and paste the real output in the PR. We do not accept "should pass" —
AGENTS.md §6: "You actually ran the commands."

Opening the pull request

  • Assignment is required before you start. Comment on this issue and wait to be assigned. Do not
    open a PR against an issue someone already holds — if it has been quiet, ask us to release it first.
  • Closes #41 in the PR description, so the issue and the PR link automatically.
  • Branch feat/…, fix/…, docs/…, test/… or chore/…. Commit subject example:
    perf(app): make SiteHeader a server component with a client island
  • M3-18 → IN PROGRESS with a row for the rest of the pass; Changelog entry; Last updated.
  • Follow .github/PULL_REQUEST_TEMPLATE.md (AGENTS.md §7). In "Hard rules / invariants touched",
    give numbers, not prose.
  • Small and focused: one logical change. Do not mix a refactor into it.
  • Do not add the security-sensitive label — that is the maintainer's flag for contract-logic PRs.
  • Do not reformat or touch files outside Scope. Unrelated diff noise costs review time and can
    break the roadmap merge.
  • Never commit to main; open a PR. Never force-push.
  • Evidence for the PR: Numbers rather than screenshots: before/after First Load JS per route in the PR and in docs/PERFORMANCE.md, plus a screenshot of the header only if markup changes.

How we review

Two maintainers: one reviews correctness against the hard rules and the roadmap row's Done when, the
other reviews diff scope and whether the tests were actually run. Order is hard-rule correctness → tests
really executed → diff stays inside Scope → style last. First response on a PR: 2 business days.

What fails review fastest: a change outside Scope, a deleted or weakened assertion, a claim with no
command output behind it, and a ROADMAP.md with no Changelog entry (AGENTS.md §12).
For this issue, look first at that the measurement method is identical before and after, and that the split did not move the whole header into a client boundary by another name.

Tier and effort

tier/medium · roughly 1–2 days. Wave complexity tag: Medium = 150 points — the published mapping is Trivial 100 · Medium 150 · High 200
(Drips' maintainer guide); the cycle's
own values are confirmed on drips.network/wave/stellar before
the Wave opens. Kinlock's tier rules and what each tier means for review:
docs/wave-issue-format.md.

Related roadmap rows

M3-18 — Mobile and low-bandwidth performance pass (TODO)

M3-18 depends on M3-13 (DONE). This is one increment of the pass; leave the row IN PROGRESS and name what is left.

Questions

Comment on this issue — a maintainer answers within 2 business days. Say "I'll take this" so we can flag it on
the Wave board and stop two people doing the same work. If you get half way and stall, post what you
have as a draft PR rather than disappearing; a blocked draft is more useful to us than silence.

Activity

  1. added
    waveScoped for Drips Wave contributors
    area:appOwnership: kinlock-app
    tier/mediumWave tier: standard feature or fix work, a few hours, tests included
    on Oct 8, 2026
  2. jdjioe5-cpu commented on Oct 17, 2026

    @jdjioe5-cpu

    I'll take this! Will measure before/after First Load JS, split SiteHeader into a server component with a minimal client island for navigation and menu interactions, preserve all WCAG/a11y/focus behaviours, document results in docs/PERFORMANCE.md, and update M3-18 in ROADMAP.md.

  3. jdjioe5-cpu commented on Oct 17, 2026

    @jdjioe5-cpu

    PR is ready for review: #49 - Refactored SiteHeader into a Server Component, extracted dynamic routing and mobile menu into NavLinks client island, preserved all a11y/Escape/reduced-motion behaviors, documented measurements in docs/PERFORMANCE.md, updated M3-18 to IN PROGRESS in ROADMAP.md, and verified full clean build and tests.

  4. added
    drips-waveDrips Wave bounty task for Stellar ecosystem contributors
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:appOwnership: kinlock-appdrips-waveDrips Wave bounty task for Stellar ecosystem contributorstier/mediumWave tier: standard feature or fix work, a few hours, tests includedwaveScoped for Drips Wave contributors

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions