Skip to content

perf(mainweb): ship the rest of the portal's first data with the HTML - #582

Merged
aamoghS merged 9 commits into
devfrom
perf/mainweb-portal-rest
Nov 6, 2026
Merged

aamoghS merged 9 commits into
devfrom
perf/mainweb-portal-rest

Conversation

@aamoghS

@aamoghS aamoghS commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Finishes the signed-in portal speed work from #580: the remaining pages now ship their first data with the HTML instead of fetching it after hydration. Also fixes a pre-existing 500 on the admin roster search.

Changes

  • PrefetchedPage server wrappers (same pattern as perf(mainweb): ship the signed-in portal's first data with the HTML #580) for the hackathons list, submit, admin home, admin attendees, admin hackathons and one hackathon's admin dashboard, admin judging and an edition's console, admin bootcamp, lead and lead/[id], initiatives, subteams, club/bootcamp, judge/register.
  • PrefetchedPage takes an async reads function for pages whose later reads depend on a first one (submit, dashboard, lead/[id], admin hackathon); a throw there costs the prefetch, never the page.
  • Dashboard prefetches its next-step reads (team.submissionWindow, team.mySubmission, judging.hacker.itinerary, hackathon.getResults); club prefetches member.myPass. stripe.attemptAutoLink (a mutation) is untouched.
  • Dates in the newly server-rendered trees use lib/venue-time (America/New_York) so server and browser agree. RegistrationControls prints in the organiser's own zone, which the server cannot know, so AttendeesTab draws it after hydration.
  • admin/members skipped: it reads nothing until the admin types.
  • Fix: hackathon.adminGetAttendees with a search term returned 500 (uuid = text). Inside the relational query Drizzle rendered the user subquery's columns against the participants alias; the user table now has its own alias.
  • Unchanged: auth redirects, role gating, proxy.ts private no-store caching, schema.

Measurements

Production build, server TZ=UTC, local Docker test DB, signed in, Playwright time until the page's content is visible, 5 alternating runs, median. Busy machine.

Page Cold Warm
hackathons list 436 to 334 ms 190 to 158 ms
submit 878 to 488 ms 412 to 232 ms
dashboard (hacker next step) 1247 to 530 ms 420 to 279 ms
club (member pass) 514 to 290 ms 203 to 150 ms
admin 493 to 308 ms 223 to 131 ms
admin/attendees 452 to 393 ms 226 to 165 ms
admin/hackathons 1113 to 359 ms 218 to 152 ms
admin/hackathons/[id] 672 to 480 ms 315 to 189 ms
admin/judging 560 to 365 ms 240 to 171 ms
admin/judging/[hackathonId] 963 to 495 ms 358 to 181 ms
admin/bootcamp 858 to 613 ms 321 to 251 ms
initiatives 437 to 301 ms 197 to 135 ms
lead 582 to 369 ms 268 to 180 ms
lead/[id] 695 to 432 ms 374 to 253 ms
subteams 643 to 475 ms 350 to 244 ms
club/bootcamp 621 to 392 ms 290 to 185 ms
judge/register 699 to 419 ms 260 to 260 ms

Testing

  • Hydration: 20 page views x 4 browser zones (New York, LA, Kolkata, Auckland) against a UTC server, seed dates straddling UTC midnight: 0 React [pull] dev from main #418/Override gaxios's uuid to 11.1.1 (Dependabot advisory) #423/Refresh the data each admin and submit action changes #425, no console errors.
  • Behaviour vs base at 402 and 1440 for anonymous, member, hacker, admin, judge and non-member visitor: identical text, redirects and theme class.
  • Flows not covered before, all passing: event check-in (200, repeat 409), hackathon event check-in by link and typed code (valid, invalid, not-yet-arrived), judging claim link (claim, then "no longer works"), door scan find-by-name and admit, club scan by email, admin event code and judging QR pages, judge activate link (anonymous and signed in, bad signature), guest judge prompt, hackathons/[id] as a signed-in non-member (all tabs, no register button). Camera scanning could not be driven.
  • Roster search on a production build: "example.test" 8 rows (via user email), "casey" 1, "zzzz" 0; 500 before.
  • tsrs --noEmit, oxlint, vitest (364 + 4 tests).

Note

Medium Risk
Large-scale PrefetchedPage refactors across admin surfaces can misalign with client hooks, while the Drizzle alias change touches attendee search SQL; both need regression verification.

Overview
Summary
Extends the portal’s PrefetchedPage wrappers so dozens of admin, club, judging, and hackathon routes hydrate with their initial tRPC payloads already in the HTML; dashboard/club pages now chained-prefetch follow-up reads (next-step hackathon queries and member pass). Shared portal components date rendering was tuned to venue time helpers, hackathon attendee tooling waits for client hydration before time-zone-sensitive controls, and bootcamp/admin UIs picked up venue formatting tweaks.

Bugfix
Hackathon admin attendee search now aliases the nested users table so Drizzle stops emitting uuid = text comparisons that broke searches with 500s.

Risk
Medium: broad server prefetch orchestration plus SQL aliasing regressions could surface if query keys diverge from client hooks or prefetch logic mis-targets user state.

Reviewed by Cursor Bugbot for commit 83197e4. Bugbot is set up for automated code reviews on this repo. Configure here.

The list drew only after hydration plus a tRPC batch. A server page now reads
the list, the visitor's registrations and projects and hands them to the
unchanged client page; its dates print on the venue calendar so the server
(UTC) and the browser agree. Cold load to the first card, 5 runs, 1440px,
median: 436 to 334 ms (warm 190 to 158).
The page waited on the registrations, then on the submission, the event and
the submission window for the event it picked. The server page reads the
registrations, picks the same event (the URL's, else the first still open),
and reads the other three with it. PrefetchedPage accepts an async reads
function for this. Cold load to the submission notice, median of 5:
878 to 488 ms (warm 412 to 232).
…mber pass

The dashboard's next step waited on the registrations and then up to four more
queries; the server page works out which the client will ask for (the same
checks as the page) and reads them. The club page also read member.myPass
after load; it is prefetched now. Cold load median of 5: dashboard checked-in
hacker 1247 to 530 ms (warm 420 to 279), club pass QR 514 to 290 (warm 203 to
150).
… list

Each drew its list only after hydration plus a tRPC batch. Server pages read
events.listAll, hackathon.listAll and the bans list for the unchanged client
pages, and the dates they print move to the venue calendar. Cold load median
of 5: /admin 493 to 308 ms, /admin/attendees 452 to 393, /admin/hackathons
1113 to 359.
…cations

The dashboard waited on the edition, then on the applications page and the
stats. The server page reads the edition, the first page of applications and
the analytics. The dates in the applications table move to the venue calendar,
and the registration controls (a deadline and inputs in the organiser's own
time zone, which the server cannot know) are drawn after hydration so both
renders agree; without that gate React threw #418. Cold load median of 5:
672 to 480 ms (warm 315 to 189).
The edition console waited on the overview and then the checklist. Server
pages read the edition list, the overview and the checklist. shortTime moves
to the venue clock so server and browser print the same times. Cold load
median of 5: /admin/judging 560 to 365 ms, an edition 963 to 495.
Attendance and the workshop table drew after hydration plus a tRPC batch.
A server page reads both for the current term and the session dates print on
the venue calendar. Cold load median of 5: 858 to 613 ms (warm 321 to 251).
… sign-up

Each page held a loading screen until its first tRPC batch returned. Server
pages read the first reads of lead, lead/[id], initiatives, subteams,
club/bootcamp and judge/register for the unchanged client pages. Cold load
median of 5: lead 582 to 369 ms, a project 695 to 432, initiatives 437 to 301,
subteams 643 to 475, club/bootcamp 621 to 392, judge/register 699 to 419.
Every search on the admin roster answered 500: "operator does not exist:
uuid = text". The filter is shared by the paged roster, which runs as a
relational query (db.query.hackathonParticipants.findMany), and inside that
Drizzle renders every column in a raw sql template against the participants
alias. The user-name and email subquery therefore compared
"hackathonParticipants"."id" (uuid) with its own user_id (text), and read
name and email off the participants alias too.

The user table now has a hand-written alias in that subquery. Checked on a
production build against the local Docker database: "example.test" matches
the 8 seeded attendees through their user email, "casey" 1, "zzzz" 0.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Visit the preview URL for this PR (updated for commit 83197e4):

https://hacklytics2027--pr-582-frbtwl8r.web.app

(expires Fri, 13 Nov 2026 17:49:49 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: c48ba34db61581e25fe2978355160b5eefe0e83f

@aamoghS
aamoghS merged commit 7cd2942 into dev Nov 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant