Reduce eager initial-load payload and repeated response computation #8
Labels
No labels
architecture
cleanup
dependencies
performance
priority: high
priority: medium
reliability
security
testing
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
solvreven/FPL#8
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Finding
The initial page load eagerly fetches and parses the complete player projection graph before the UI becomes ready:
frontend/app.js:302-360blocks startup on/api/playersand/api/status, then starts matches, accuracy, live refresh, squad/rules, fixtures, market, overrides, and teams.backend/web/main.py:305-368rebuilds a large derived dictionary for every/api/playersrequest, including per-gameweek rows, six-week intervals/SD, components, ownership fields, and differential scores for every player.backend/web/main.pyrecords/api/playersat 1,562,646 bytes uncompressed. Gzip reduces transfer size, but not server serialization, browser decompression, JSON parsing, allocation, or retaining the entire object graph.refreshLiveFast(null, true)also initiates a live upstream FPL fetch on every page load, even when the server-side live data is within its documented 900-second TTL.Suggested direction
/api/refresh-livehonor freshness server-side so repeated clients do not stampede the upstream API.Acceptance criteria
/api/players, payload/parse cost, and initial readiness.A few measurement and compatibility choices are needed before changing the payload contract:
/api/playerschange shape and require the current frontend to migrate atomically, or must it remain backward compatible while summary/detail endpoints are introduced?Without these, several incompatible implementations could each appear to satisfy the issue.
Answering all four, with the payloads measured on the running app just now.
Measured first, because it changes the issue. With
Accept-Encoding: gzip, which every browser sends:GZipMiddlewareis installed and working. The wire is already 90% solved, so "reduce the payload" measured in bytes is largely done and is not where the remaining time goes. What is left is ~2 MB of JSON parsed and 623 players × 26 fields turned into objects before anything renders. That reframes every question below.1. Baseline environment: local uvicorn + Chromium. Not a choice so much as a fact — there is no deployed host; the app binds 127.0.0.1 and
require_localenforces it. Measure with the Playwright harness the suite already uses (tests/web/test_frontend_render.py), so the baseline is reproducible by anyone running the tests rather than a number from one machine.2. Primary threshold: time from navigation to the Squad tab being usable — first paint of the pitch with real numbers — not payload bytes, not CPU, not retained heap. Bytes are already compressed; heap is not what the reader experiences; CPU on a single-user local app is not scarce. "Materially reduced" should be stated as an absolute, not a percentage: under 1.5s cold on the baseline above. Take the current figure first and put it in the issue, so the target is a diff and not an aspiration.
Secondary, worth recording but not gating: parse time for
/api/playersspecifically, since that is the single largest contributor and the one a summary endpoint removes.3.
/api/playersmay change shape, but NOT atomically. Add/api/players/summarycarrying only what the tables and pitch need (id, name, team, position, price, ev6, start_prob, badges, ownership), migrate readers one surface at a time, and keep/api/playersserving the full rows until nothing reads it — then delete it in its own commit. An atomic swap makes the change unreviewable and unbisectable, which is the same objection this repo raised to the refactor in #7.Note the per-gameweek rows are the bulk, and the player CARD is the only surface that needs them. That is the natural seam: summary for lists, full row on open. It also fixes a real thing — the card currently renders from data fetched for 623 players to display 1.
4. In-process is sufficient. There is one uvicorn process;
set_deps/set_statealready hold shared state in module globals and the autorefresh thread mutates them. Adding cross-process caching would mean adding a second process first, which nothing here wants. If that ever changes, the cache is not the hard part — the shared mutable deps are.One caution. The app already carries a serialisation guard (
202b69e) and an mtime-based cache hot-reload. A response cache keyed on anything other thancache.built_at+ the deps generation would serve projections from before a rebuild while the header claims the new build time — the exact stale-artefact failure CLAUDE.md is organised around. Key it onbuilt_at, or do not add it.