Skip to content

Performance round 4: engines and engine code only where needed, headings at once - #326

Merged
ashtonanderson merged 9 commits into
mainfrom
claude/perf-round-4
Nov 3, 2026
Merged

ashtonanderson merged 9 commits into
mainfrom
claude/perf-round-4

Conversation

@ashtonanderson

@ashtonanderson ashtonanderson commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Fourth round, after #324: the page-loading costs Lighthouse found on maiachess.com (phone scores 41–70). Details and all numbers in LOADING.md ("Round 4").

Changes

  • Engines only where they're needed (8383bc0).
    • The /broadcast list started Stockfish and Maia: _app.tsx matched any path containing /broadcast, so a first visit downloaded Stockfish's 76 MB of nets just to show a list. It now starts no engine; a round starts them when it opens.
    • On /drills and /puzzles, which need Stockfish only after a while, it now starts once the page has loaded and the browser is idle (deferStart).
  • Engine code only on engine pages (017694c). _app.tsx imported the Stockfish, Maia and analysis-list providers, so every page downloaded and ran both engines' code and Maia's move tables (111 KB, parsed on every load). Engine pages now list the providers they need (Page.providers, src/contexts/pageProviders.ts). First Load JS shared by all pages 308 → 235 kB.
  • Headings at once (1ab0ce1, 8fb8652).
    • The leaderboard and the broadcast list rendered nothing, on the server too, until their data arrived.
    • The leaderboard now renders its heading and columns at once, with placeholder rows until the rankings arrive.
    • The broadcast list renders its heading while Lichess answers, and no longer waits for the visitor's session (it asks only Lichess).
  • Home page counters (181412c, d6ef8b9): a fixed count-up instead of a spring that took ~3 s to settle, re-laying out the hero on every frame; 1.5 s in 181412c, shortened to 0.5 s in d6ef8b9.

Results (main → branch)

Emulated phone (4G, every thread at a quarter speed), medians of 5 loads:

/broadcast list, first visit no 76 MB download; main-thread work 9.2 → 4.3 s; main content 4.8 → 0.7 s
/leaderboard main content 3.2 → 0.9 s (first visit), 1.8 → 0.8 s (returning); main-thread work unchanged
Home, first visit main-thread work 11.0 → 7.9 s; JavaScript 636 → 474 KB
/turing, /play, returning main-thread work 2.8 → 2.2 s, 1.9 → 1.5 s
/drills, /puzzles, returning Stockfish starts 0.8–1.6 s later; main-thread work 5.0 → 3.9 s, 6.0 → 5.7 s

Home on a desktop: main-thread work over the load and 6 s 2.65 → 2.04 s (counters at 1.5 s).

The home page's other costs, measured by leaving each out (phone, main-thread work): without the Play section's 36-board grid 6.8 → 5.8 s on a first visit, 5.1 → 4.3 s returning (desktop 1.29 → 0.94 s); without the game carousel, no measurable change. Neither is removed here.

Lighthouse, phone settings, local builds, medians of 3:

page score
home 58 → 67
/broadcast 43 → 71
/leaderboard 52 → 73
/turing 51 → 64
/play 72 → 76
/blog, /drills, /puzzles no change

Stockfish

Not slower (scripts/perf/chart-probe.mjs, 8 moves per run, analysis page):

main → branch
Desktop: depth 12 after a move 0.04 ± 0.05 s sooner (16 of 32 moves)
Desktop: depth reached before the next move 16.66 → 16.88
Phone: depth 12 after a move 0.06 ± 0.22 s sooner (27 of 47 moves)
Phone: moves reaching depth 12 48 → 47 of 48
Phone: depth reached before the next move 14.62 → 14.54

Testing

  • npm test (84, with main's), npx tsc --noEmit and npm run lint pass, rebased on main after Capture PostHog web vitals again #330 and Track what visitors do, and stop losing events #332.
  • Checked in local production builds:
    • Analysis gets Stockfish's evaluation, loaded directly and after an in-app move from the home page (0.76–1.0 s on both builds).
    • Puzzles show Stockfish's evaluation as soon as a puzzle is given up.
    • A drill started right away loads as on main.
    • The broadcast list requests no engine file, and opening a round from it starts Stockfish.
    • The leaderboard's placeholder rows line up with the loaded rows (layout shift 0.0001).
  • Not tried on real phones or in Safari.

Not done

Shells for the app pages. /puzzles, /drills, Bot or Not and analysis still send only the header and footer. They wait for the visitor's session and their data, and choose the phone or desktop layout in JavaScript after loading. A server-rendered shell would show the desktop layout on a phone and then jump, so it first needs that switch moved to CSS.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
maia-platform-frontend Ready Ready Preview Nov 3, 2026 6:32am UTC

Request Review

ashtonanderson and others added 8 commits November 2, 2026 18:47
… on drills and puzzles

The broadcast list started Stockfish and Maia (Stockfish's 76 MB of nets on
a first visit) because any path containing /broadcast matched; a round now
starts them when it opens. /drills and /puzzles need Stockfish only after a
while (a drill is picked first; a puzzle is analysed in the background),
so there it starts once the page has loaded and the browser is idle,
instead of competing with the page's own requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_app imported the Stockfish and Maia providers (and so both engines'
code and Maia's move tables) and the analysis list provider, so every
page downloaded and ran them. Engine pages now list the providers they
need (Page.providers, see src/contexts/pageProviders), and _app adds
them. First Load JS shared by all pages 308 -> 235 kB (_app 141 ->
67 kB); home 295 -> 235, /leaderboard 296 -> 223, /blog 284 -> 210 kB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… at once

Both pages rendered nothing (on the server too) until their data
arrived, then faded in from transparent, so their headings showed only
after the request. The leaderboard now renders its heading, columns and
note at once, with placeholder rows until the rankings arrive; the
broadcast list renders its heading and a short note while Lichess
answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion

The list asks only Lichess, but sat in AuthenticatedWrapper, which renders
nothing until the account request returns (and nothing on the server).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hero's three counters sprang from 0 to tens of millions, which took
about 3 s to settle and re-laid out their text on every frame of it (the
duration prop was ignored by the spring). They now tween in 1.5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…usel measured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase on #330 and #332 gave round 4's commits new IDs, and the
old ones are no longer on any branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashtonanderson
ashtonanderson marked this pull request as ready for review November 3, 2026 06:34
@ashtonanderson
ashtonanderson merged commit 503c123 into main Nov 3, 2026
6 checks passed

This branch was successfully deployed

1 active deployment
Preview — 03e9dbdf Deployed Nov 3, 2026 by vercel[bot]
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