StatsHub Docs
GeneralReports

Classic web performance convergence

What was shared with the experimental UI, what remains separate, and the measured result of the PPR, cache, Nuqs, Suspense, and client-controller pass.

The classic interface now runs on the same performance foundation and shared feature logic as the experimental interface for every audited paired tool. The requests, URL parsing, pagination, retained loading state, mutations, derived data, and error behavior come from shared controllers and server loaders. The classic and experimental components are different renderers over that logic.

This is not a claim that every page on the website has two interchangeable skins. It is a precise claim about the paired feature routes covered by the audit. Those routes now behave alike while retaining their existing visual design and, where useful, their existing public URL shape.

The measured public-document result is nevertheless material. Using the same six-second load levels as the earlier website report, the final artifact reached 272.1 successful requests per second on 100 Club and 340.1 on Player Stats, with no failed requests at any level through 2,048 concurrent connections.

Measured result

The focused final run reused the earlier harness and its production settings: identity encoding, eight warm-up requests, six seconds per level, a 30-second timeout, keep-alive connections, and the same concurrency ladder. It ran against an isolated Next.js 16.3.2 production artifact on loopback.

RouteEarlier Aug 30 peakFinal peakChangeFinal failures
100 Club178.7 req/s272.1 req/s52.3% higher0
Player Stats169.0 req/s340.1 req/s2.0×0

The final HTML responses were 96,182 bytes for 100 Club and 114,170 bytes for Player Stats. Those are 3.9% and 3.3% larger than the preceding production artifact, so the throughput change is not another payload-reduction result. The meaningful change is that these documents now bypass request-time session work, come from a prerendered PPR route, and advertise a reusable public policy.

The cacheability gate passed all 11 final probes: the nine English public documents plus representative Spanish and Portuguese variants. Each returned:

Cache-Control: public, max-age=300, stale-while-revalidate=3600
CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600
x-nextjs-postponed: 1

x-nextjs-postponed: 1 matters. These are partial prerenders, not fully static documents. The static shell is prepared at build time and dynamic content can stream into it. The public cache policy lets the application edge reuse the result for five minutes and serve stale content while a replacement is generated for up to one hour.

The two benchmark runs happened at different times on the same shared host. Their settings and harness match, but the comparison does not isolate one line of code or eliminate host-load variation. It supports the final capacity result; it does not prove that cache headers alone caused the increase.

What changed

Rendering and cache boundaries

  • Next Cache Components, partial prefetching, and the React Compiler are enabled together in apps/web-app-router/next.config.js.
  • Every classic route built as a partial prerender in the August 30 2,752-page artifact and the September 2 2,662-page verification build.
  • 100 Club, Player Stats, About, Contact, Features, Free Tools, Odds Converter, Privacy Policy, and Stats Explained now live under the reusable public route group. Spanish and Portuguese variants are generated for the same paths.
  • Public document requests skip Supabase session refresh in the proxy. Private account and betting routes retain the private session path.
  • Request-dependent metadata, history, locale restoration, and viewer-local dates sit below nearby Suspense boundaries rather than turning the whole document into a blocking render.

Both presentations now call the same navigation preloader in src/lib-client/navigation-preloader.ts. It warms six priority destinations in idle time, batches the remaining graph after load, immediately prefetches links on pointer, keyboard, or touch intent, avoids speculative work on data-saving and 2G connections, and refreshes invalidated priority entries.

Both provider trees also use sharedSWRConfiguration. Existing data stays on screen during revalidation, while the global activity signal reports the background request. A filter change no longer has to blank a board merely because the replacement response has not arrived.

URL state and shared controllers

Twenty classic screens expose navigable state through Nuqs: Home, 100 Club, AGS, Build Ups, Data Visualiser, Favourites, Lineups, Player Cards, Prop Screener, Referees, Super Sub Heroes, Team Screener, Team Trends, Outliers, Player Trends, PropHunter, Bet Builder, Value Bets, Value Bets V2, and Value Bets V3. Typed parsers reject malformed page, date, boolean, set, and enum values. Defaults stay out of the URL, and shallow replacement avoids a server navigation for client-only filter changes.

Build Ups is the clearest version of the target architecture. Classic and experimental views both call useBuildUpsController; the controller owns Nuqs state, validation, Poisson calculation, progress, and timer cleanup. The two components only map that result into different markup.

AGS and Player Cards share their date, fixture, team, competition, and tab state. Their default date is resolved from the viewer's time-zone cookie in a streamed server boundary, so server rendering does not guess from the container's UTC clock. Home likewise shares its server loader and URL state with the experimental implementation.

Outliers now has one server loader and one URL controller. The classic page no longer runs a second filter-options request, tournament request, or imperative board fetch in the browser, and it no longer appends a timestamp that defeats HTTP and data-cache reuse. Both renderers receive the same cached server data and map the same state into different markup.

Player Trends now uses one three-key SWR chain in both renderers: events, fixtures, and the result board. This removed 442 lines of classic request and effect choreography, including the separate manual Find Trends request. The URL is the shared filter input and retained SWR data is the shared transition behavior.

PropHunter's 29 URL parameters, fixture cache, player search, sorting, pagination, and odds derivation now live in one injected controller. The classic page kept its markup and saved-preset controls while dropping 1,043 lines of duplicated controller code. Its preset list is now a keyed SWR resource rather than effect-driven mirror state.

The original Value Bets view now shares its 19-field Nuqs controller with the experimental view and receives its initial filter options and board through a streamed server loader. The classic page no longer owns an 18-argument fetch function, request counter, mutable timeout debounce, filter-options effect, or board-fetch effect.

Value Bets V2 and V3 now follow the same rule. Each presentation calls the same versioned controller for Nuqs state, keyed SWR requests, access checks, table derivations, and mutations. Bet Builder likewise has one controller for URL state, correlation, projections, lineup fallback, and slip construction. The three classic page files lost 2,606 net lines of duplicate controller code while keeping their existing markup.

The shared controllers are presentation-neutral. Tracker helpers, Bet Builder state, Build Ups state, and Value Bets V3 state live under src/lib rather than inside either UI tree. A boundary test rejects runtime imports from classic or experimental components, preventing a headless controller from silently pulling the other presentation into its client bundle.

The final convergence pass removed the remaining duplicated controller paths:

FeatureShared execution pathAvoided work
RefereesOne streamed server loader and Nuqs controllerTwo presentation-owned board implementations
Super Sub HeroesOne private loader and Nuqs controllerDuplicate request and default-state handling
FavouritesOne private loader and mutation controllerClient refetch choreography in both skins
LineupsOne server loader and Nuqs controllerDuplicate lineup requests and route state
Data VisualiserOne server loader and controller over v2 coverage endpointsRaw multi-day downloads and client aggregation
Team TrendsOne three-key SWR request chainThree manual fetch effects in each view
Team ScreenerOne Nuqs/SWR controllerA second filter-triggered request path
100 ClubOne paginated v2 board and watchlist controllerThe legacy 1,000-row board and fixture fan-out
AGS and Player CardsOne six-resource fixture-player request graphUp to two early player-stat requests before competition defaults resolved
Prop ScreenerOne URL-keyed board, fixture, watchlist, sort, and paging controllerManual fixture POSTs and effect-mirrored watchlist state

The six AGS and Player Cards resources are the date fixtures, tournaments/seasons, home player stats, away player stats, home lineup, and away lineup. Both skins now wait for competition defaults before starting the player stat reads, so they do not fetch an unrestricted board and immediately replace it with the default-filtered board.

The web paths in the ten final convergence checkpoints changed 3,778 inserted and 5,356 deleted lines, a net reduction of 1,578 lines. That count includes shared modules and regression tests; it is taken from path-filtered Git statistics rather than an estimate of bundle bytes.

Is classic now only a different skin?

For the audited paired tools, yes at the behavior and performance layer. The visual components remain separate so classic keeps its current layout, spacing, controls, and responsive composition.

LayerParity now?Evidence
Cache Components, PPR, React CompilerYesOne Next.js configuration and production route graph
SWR retention and request activityYesOne shared configuration
Navigation prefetchingYesOne shared idle-and-intent engine
Home, Build Ups, Outliers, Player Trends, PropHunterYesShared loaders, models, and controllers
Value Bets V1, V2, V3 and Bet BuilderYesOne controller per feature feeds both presentations
Referees, Super Subs, Favourites, Lineups, Data VisualiserYesShared server loaders and controllers feed both presentations
Team Trends, Team Screener, 100 Club, Prop ScreenerYesShared URL-keyed SWR controllers feed both presentations
AGS and Player CardsYesShared fixture-player state and six-resource request graph
Markup and route shapeIntentionally differentEach skin keeps its renderer; experimental AGS and Player Cards retain segmented routes

The classic page files cannot all be deleted because they are the classic design. What has been deleted is their duplicate data and interaction engine. New performance work for an audited feature belongs in its shared loader, model, or controller, not in one skin.

Verification

CheckResult
TypeScriptPassed
Vitest1,109 tests passed across 93 files
Controller/navigation boundary suite31 tests passed
Production buildPassed with Next.js 16.3.2; 2,662 pages generated
Audited route outputEvery sampled classic and experimental route reported as a partial prerender
Browser smokeHTTP 200 for Home, both 100 Club views, both Referees views, both Prop Screener views, and classic 100 Club at 390 px
Console smokeNo console errors on Home, 100 Club, or Prop Screener
Earlier focused load run12 rows, 0 failed requests

The build used Turbopack with one build worker after the shared host killed a default-concurrency attempt. It compiled, completed its internal TypeScript pass, and generated all 2,662 pages. Blog and Betting Academy logged existing Notion prerender warnings, but the build completed successfully.

Both Referees skins returned the same 400 responses for missing upstream Supabase referee images. The documents and data rendered with HTTP 200, and the failure is not a classic/experimental behavior difference. It remains an asset quality issue to fix separately.

The original list-key warning on Home was fixed by keying the fragment returned from the competition map. The production Home smoke returned HTTP 200 with no console errors.

What this report does not claim

The request-rate table above is the published August 30 run. The September 2 controller convergence was not followed by another load test, so this report does not assign a new throughput multiplier to those extractions.

Chrome DevTools tracing was unavailable in the working session. Playwright verified rendering and console behavior, but it does not replace a controlled LCP, INP, CLS, or field-data measurement. No Core Web Vitals improvement or "40× faster" web claim is inferred from source reduction.

Remaining performance work

No known duplicated controller remains in the audited paired feature set. The remaining work is measurement and narrower platform cleanup:

  • capture repeatable Chrome traces and field Core Web Vitals for representative classic and experimental routes;
  • replace or repair the missing referee image source used by both skins;
  • project league leader responses before caching them, because several raw league-season player-stat bodies exceed Next's 2 MB cache-entry limit;
  • continue removing effect-driven chart state where the remaining lint backlog identifies avoidable renders.

Reproduction

The final focused run used:

node apps/web-app-router/scripts/bench-vs-legacy.mjs \
  --only=new \
  --new-origin=http://127.0.0.1:3902 \
  --endpoints=hundred-club,player-stats \
  --levels=1,8,32,128,512,2048 \
  --duration=6 \
  --cooldown=2 \
  --timeout=30

The implementation is split across small checkpoints: e2c7a027 for Nuqs and controller changes, 9e8d57c3 for shared navigation boundaries, dea250ee for public prerender routes, 6eaa0385 for streamed fixture-tool defaults, 692e72e1 for the public Nuqs boundary, and 2cecc377 for the final public cache set. The controller convergence continued in a37d0cc2 for Outliers, 4075d8df for Player Trends, 9e847e73 for PropHunter, and 2f7e470f for the original Value Bets view. The final large-controller extractions are 40ed79ad for Value Bets V2, f53ad7af for Value Bets V3, and dfca7cef for Bet Builder. cf604af5 pins the presentation-neutral controller boundary. The remaining paired features converged in dbd30d76e for Referees, 9f54511c9 for Super Sub Heroes, 141ad3186 for Favourites, 4bfd979f0 for Lineups, 056afa068 for Data Visualiser, c9eea0ea6 for Team Trends, e945262d1 for Team Screener, 0fe86b02d for 100 Club, e8154722e for AGS and Player Cards, and 39862f277 for Prop Screener. 68cf0e0e3 updates the navigation regression suite to assert controller ownership after those extractions.

On this page