Skip to content

perf(nba): load roster once server-side, stop shipping full CSV to client#126

Open
FarhanAliRaza wants to merge 5 commits into
mainfrom
csv-data-fix
Open

perf(nba): load roster once server-side, stop shipping full CSV to client#126
FarhanAliRaza wants to merge 5 commits into
mainfrom
csv-data-fix

Conversation

@FarhanAliRaza
Copy link
Copy Markdown

@FarhanAliRaza FarhanAliRaza commented Jun 1, 2026

What

Refactors the nba template's backend to stop reloading and re-serializing
the entire player roster on every page load.

  • Load the roster once at import time into a module-level PLAYERS
    constant instead of running State.load_entries on every page load and
    storing the full list in per-connection state.
  • Stop sending the entire roster over the websocket on load. Because the
    full player list lived in state, all ~480 rows of CSV data were serialized
    to the client on initial connection. Only the current page (12 rows via
    get_current_page) now crosses the wire.
  • Split the monolithic State into a table-only State and a chart-only
    StatsState, so chart data is never computed or sent to the client while
    the Table tab is showing.
  • Replace pandas / numpy (used only for CSV parsing and np.random.choice)
    with stdlib csv and random, removing both dependencies.
  • Collapse the duplicated per-chart aggregation loops into shared
    _passes_filters / _average_by helpers.

Impact (measured, prod build, 3 profiling passes)

Metric Before After Change
WS payload onload 156.6 KB 17.0 KB −89%

Load the roster once at import time instead of re-reading the CSV into
per-connection state on every page load, and split the monolithic State
into a table-only State and a chart-only StatsState so chart data is
never computed or sent to the client while the Table tab is showing.
Filtering/aggregation now uses stdlib csv and random plus shared
_passes_filters / _average_by helpers, removing the pandas and numpy
dependencies entirely.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR refactors the NBA template to load the player roster once at import time into a module-level PLAYERS constant, eliminating a per-connection CSV re-read and stopping the full roster (~480 rows) from being serialised over the websocket on every page load. The monolithic State is split into a table-only State and a chart-only StatsState, and pandas/numpy are replaced with stdlib csv and random.

  • PLAYERS is a server-side constant; only the 12-row current page crosses the wire via the _filtered_sorted_players backend var, giving the 11x load-time improvement reported in the PR.
  • Duplicated per-chart aggregation loops are collapsed into _passes_filters / _average_by helpers, substantially reducing backend.py.
  • total_pages still computes from the fixed total_items = len(PLAYERS) rather than the current filtered count, so pagination controls over-report available pages during a search.

Confidence Score: 3/5

Safe to merge for the performance wins; the pagination count mismatch during search needs attention before it would be considered fully correct.

The roster load and chart-data refactoring are well-executed. The main concern is that total_pages derives from the static total_items rather than the length of the filtered player list, so typing a search term and then clicking Last Page navigates to an empty result set.

nba/nba/backend/backend.py — the total_pages computed var and the salary sort key for NaN sentinel players.

Important Files Changed

Filename Overview
nba/nba/backend/backend.py Core refactor: replaces pandas/numpy with stdlib csv/random, loads the roster once into a module-level PLAYERS constant, splits State into table-only State and chart-only StatsState, and adds _passes_filters/_average_by helpers. total_pages still references the fixed total_items instead of the filtered count, breaking pagination during search.
nba/nba/components/item_badges.py Mechanical rename of State to StatsState for all badge event handlers and cond expressions; no logic changes.
nba/nba/components/stats_selector.py Mechanical rename of State to StatsState for all selector event handlers and reactive bindings; no logic changes.
nba/nba/nba.py Removes the on_load=State.load_entries handler; data is now ready at import time so no page-load event is needed.
nba/nba/views/stats.py Removes the local StatsState definition (moved to backend.py) and updates all chart data bindings from State.* to StatsState.*.

Comments Outside Diff (2)

  1. nba/nba/backend/backend.py, line 117-121 (link)

    P1 total_pages is computed from the fixed total_items = len(PLAYERS) and never reflects the current search-filtered count. When a user types a search term that narrows the roster to, say, 5 players, total_pages still computes ~40 pages, so next_page and last_page happily navigate into empty slices. Since _filtered_sorted_players is already a @rx.var(cache=True), its length is the natural source of truth for pagination.

  2. nba/nba/backend/backend.py, line 86-92 (link)

    P2 When sort_value is "salary", players whose salary is stored as the sentinel string "NaN" produce float("NaN")nan. Python's sorted() key comparisons involving nan are undefined — the relative order of NaN players versus any other player is unspecified and can differ across CPython releases and sort inputs. Consider placing them consistently at the end.

Reviews (1): Last reviewed commit: "refactor: drop pandas/numpy from nba tem..." | Re-trigger Greptile

FarhanAliRaza and others added 4 commits June 2, 2026 02:45
reflex is a uv workspace, so reflex and its split-out sibling packages
(reflex-base, reflex-components-*) must be installed together from the
same tree. The old `pip install reflex@git` pulled released sub-packages
from PyPI and broke whenever main moved a module between packages. Check
out the monorepo to .reflex-src and `uv pip install` it instead, and
rename the workflow input from reflex_dep (raw pip spec) to reflex_ref
(git ref).
Empty salary/college cells now parse to None (Player fields become
Optional) instead of the string "NaN", so sorting, filtering, search and
averaging work on real values without special-casing a sentinel. Missing
values always sort to the end and render as "—" in the table.

Derive total_items from the filtered list, reset offset to 0 on a new
search, and floor total_pages at 1, so the page count and navigation can
never drift past the actual matches.

Also move the Radix theme from rx.App(theme=...) to a RadixThemesPlugin
in rxconfig.
Use SQLModel directly with explicit primary-key fields, replace
get_fields()/set() with model_fields and setattr, and register
RadixThemesPlugin across template rxconfigs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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