A page that has not been visited before takes about two and a half seconds to paint anything. Measured on training.lsu.stacksjs.com/dashboard, signed in, from the Navigation Timing and Resource Timing APIs rather than by feel.
server wait (TTFB) 880 ms
HTML transfer 708 ms
DOM interactive 2337 ms
first contentful paint 2556 msThe document itself is 662 KB decoded, of which 311 KB is serialised DOM, across 29 inline scripts and a 31 KB inline stylesheet.
Where the time goes, apportioned
Worth separating, because not all of this is one component's fault.
Not stx: the server. 880 ms of TTFB is spent before the browser has a single byte. Nothing on the client can improve that.
Not stx: third party analytics. analyticshq.org/script.js took 988 ms and analyticshq.org/collect another 470 ms, on the critical path during first paint. That is roughly 1.4 s of connection time for something no user is waiting for. It wants async/defer at minimum, and arguably to be loaded after first paint.
Probably stx: the router prefetches sibling documents during the initial load. While /dashboard was still painting, the same navigation also fetched /home and /performance as full HTML documents. At this document size that is real contention, and it is happening while the page the person asked for has not painted yet. Prefetching is the right idea; doing it before the current page is interactive is not. Deferring it to idle would cost nothing and give the first paint the bandwidth.
Probably stx: module chunk size. _stx/modules.35be52f50cefdd92.js took 1151 ms and _stx/modules.d1d6fc3eb98317d1.js 975 ms. Five module chunks plus a runtime and a router chunk are requested on this page.
Small, separate: /api/auth/me is requested twice in the same page load.
Compression is fine, so that is not it
Checked, because it would be the obvious suspect. Content-Encoding: br is set and working: /login is 36,285 bytes on the wire against 182,421 raw.
Suggested order
- Defer the analytics script. Cheapest change, roughly 1.4 s off the critical path, no behaviour lost.
- Hold router prefetch until the current page is interactive, or behind
requestIdleCallback. - Then look at the 662 KB document and the module chunking, which are the larger pieces of work.
Numbers above are one sample on one machine. Worth repeating before and after each step rather than trusting a single run.