perf(admin): stop the prefetch storm behind the laggy console - #368
Merged
Merged
Conversation
The rail is seven links to seven dynamic routes and it never leaves the screen, so anything that re-prefetches costs seven server renders. One operator sitting still on one page produced eleven renders of /inbox in eight minutes of production logs. optimisticRouting defaults to true from Next 16.3.0 (vercel/next.js#97135, #85489); staleTimes.dynamic defaults to 0, so a dynamic prefetch is stale the moment it lands. Both are upstream bugs — retest on the next upgrade.
A dashboard render asks twice — the layout gates everything below it, the page gates itself — and both are right to ask. That was two identical three-table joins per request, and past half a session's life two renewal writes as well.
No endpoint emits `unavailable`. The comment above it also credited a Postgres trigger for announcements the app makes itself.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
pratyush618
approved these changes
Sep 22, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Subscribers page was showing
_rscfetches stuck on(pending)in DevTools, and the console felt laggy with it.What the logs say
vercel logs -p byteveda-site-admin, deduplicated by request id — the CLI repeats each record once per page of results, so a raw count is 42x the truth. Fifty real requests in the 8m44s window that contains the report, one operator, sitting on one page:/inbox/api/subscribers/stream/subscribers/api/inbox/stream/stats/settings/members/postsNobody opened
/inboxeleven times. Those are prefetches of the seven rail links, re-issued over and over, and each one is a full server render that reads Postgres.The changes
Stop the client router re-prefetching the rail.
experimental.optimisticRoutingdefaults totruefrom Next 16.3.0 and is the regression behind #97135, where the prefetch scheduler livelocks. #85489 is the Next team's own confirmed "links prefetch more than once on 16", whose reporter watched Vercel invocations more than double on upgrade. #97329 is the shape that shows up as a request stuck on(pending): a prefetch whose response never settles the cache entry that asked for it.experimental.staleTimes.dynamicdefaults to0, so a dynamic route's prefetch is stale the instant it lands and the next render is entitled to ask again. All seven rail links are dynamic, and all seven are on screen permanently. Thirty seconds is long enough that a link is fetched once per visit, and every one of these pages has a live stream or a server action behind it for the events that actually matter.Both settings exist only because of upstream bugs, and the comment in
next.config.tsnames them so the next person can retest on an upgrade.Memoise the session read.
getSession()was not memoised, and a dashboard render asks for it twice — once inDashboardLayoutto gate everything below it, and again in the page's ownrequirePermission(...). Both are right to ask; neither can assume the other ran. That was two identical three-table joins per request, and past half a session's life it was two renewal writes as well, which is the part that was a bug rather than a cost. React'scacheis per-request, so a suspension or a role change still takes effect on the operator's next request.Drop a listener nothing ever sends. No endpoint emits
unavailable. The comment above it also credited a Postgres trigger for announcements the app makes itself, throughsubscribersChanged.Deliberately not here
DashboardLayoutreads the session and then the unread count, and that serial hop stays: the count is scoped to the workspaces the session grants, so it cannot start first. With the memoisation in place the second read is free anyway.Folding
/api/subscribers/streaminto the inbox stream is real — two held invocations per open Subscribers tab instead of one — but it is nine of fifty requests against the thirty-seven the config change addresses, and it is a behaviour change across the console rather than a configuration one. Worth its own issue.Verification
pnpm --filter @byteveda/admin typecheck, 256 tests across 26 files,pnpm lint(exit 0, nothing in the changed files),pnpm check:arch(319 files, no violations), andnext buildwith no warning about either experimental key.The thing still worth checking after this deploys is the production logs:
/inboxshould stop appearing eleven times in nine minutes.