---
title: Close the SPA's API-response-to-store gap on league home
type: task
status: completed
description: >-
  League home paints before the /api/leagues/:id record it already holds reaches the store (~120ms
  gap), which is 70% of the logged-out page's remaining layout shift. Find where the delay goes and
  fix it at its cause.
base_uri: user:task/league/close-spa-api-response-to-store-gap.md
created_at: '2026-10-09T18:20:28.914Z'
entity_id: e1e739b9-f7d2-4300-ab4d-2d05030f1ee1
finished_at: '2026-10-10T04:32:07.434Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
relations:
  - succeeds [[███████████████████████-███-██████-████████]]
started_at: '2026-10-10T04:09:13.320Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-10T04:32:07.632Z'
visibility_analyzed_at: '2026-11-05T23:48:33.268Z'
---

## Overview

On league home, `/api/leagues/:id` answers at about 492ms but its record reaches the store at about 611ms. The first commit lands at 556ms, so the page paints without data it already holds. For a logged-out visitor this gap is 70% of the remaining layout shift (CLS median 0.2469, bimodal 0.0084 / 0.2469, nine warm runs at 1400px). The page skeletons cannot reserve the right height until the record lands.

The gap is in the SPA's data path, not in a single component. Any page that sizes a skeleton from a slice that lands after first commit may pay it too.

## Background

Measured and recorded on [[███████████████████████-███-██████-████████]] (closed 2026-10-09). That task named this as the better lever and did not investigate it. Its two rejected alternatives were a hardcoded typical league size and gating the odds skeleton on a known count. The first trades a measured shift for an unmeasured one. The second measured identical (0.2682 against 0.2680).

## Plan

- [x] **Trace the response-to-store path for `/api/leagues/:id`** — fetch resolution, saga or effect, reducer, selector — and find where the ~120ms goes
  - The probe now times each hop (network end, fetch resolved, body parsed, `_FULFILLED`) and takes `--origin` (league `f0f28996d`)
  - Finding: the saga and reducer cost about 1ms. On the dev server the response waits behind a ~147ms `vendor-react-table` evaluation, which comes from `inline-source-map`, and every API call pays a CORS preflight. On `https://xo.football` the gap is 0–1ms, and the record is in the store about 150ms before the rail in 9 of 9 anonymous runs
- [x] **Fix the delay at its cause** — closed without a code change, because production has no gap. The production layout shift has a different cause, so it moved to [[user:task/league/reserve-league-home-article-column-in-production.md]]
- [x] **Check whether other read-heavy pages pay the same gap** — in production the team page has no gap (CLS 0.084). Matchups has 70–150ms gaps with no long task, but its CLS is only 0.025

## Key locations

- `repository/active/league/scripts/measure-layout-shift.mjs` — needs `yarn dev:live` on 8091; `--token-file` / `--require-auth` select member vs anonymous
- `repository/active/league/app/core/api/` — `load-state.mjs`, `reducer.js`
- `repository/active/league/app/views/pages/league-home/index.js` — `is_odds_loading`
- `repository/active/league/docs/guides/spa.md` § Skeletons and layout shift
