Always measure a production build. The dev server serves unbundled modules and React's development build, which can be several times slower.
Ports
| Port | What | Who |
|---|---|---|
| 3100 | Dev server (bun run dev) | Any session may restart it |
| 3200 | Stable copy shared through ngrok, its own worktree | Never use |
| 3300 | Production build for performance runs (bun run perf:serve) | Performance runs |
The loop
bun run perf:serve: build and serve on :3300. Rebuild after every change you want to measure.bun scripts/perf-image-grid.ts: runs every scenario three times and prints a table of medians; the raw numbers go toperf/results/image-grid-<date>.json. Compare with the previous file.- When a number is worse or odd, ask Claude to investigate with the DevTools MCP: open the page on :3300, record a performance trace while doing the same thing (load, scroll, switch layout), and read the insights (LCP breakdown, long animation frames, layout shifts, render-blocking requests).
- Fix, rebuild, run the script again, and record the new numbers in the experiment's docs (for the image grid:
src/routes/features/image-grid/-docs/performance.md).
The script
scripts/perf-image-grid.ts drives headless Chrome (playwright-core, the installed Google Chrome) with fixed conditions: 1440 × 900 at DPR 2, CPU slowed 4×, cache disabled, a new browser context per run. Per scenario (engine × result size) it measures:
| Metric | How |
|---|---|
| LCP, CLS | PerformanceObserver, during load |
| TBT | Long tasks over 50 ms, during load |
| INP | Event Timing: slowest interaction (layout switch and back; arrow keys where there is keyboard support) |
| Scroll smoothness | Frame gaps from requestAnimationFrame (frames over 33 ms, worst frame) and Long Animation Frames (count, blocking time) over 40 wheel steps of 400 px |
| Cost | DOM nodes, images and bytes from the preview server on load, tiles in the DOM after scrolling, duplicate tiles |
Headless Chrome is used on purpose: a background tab in a normal Chrome pauses IntersectionObserver and animation frames, which stops load-more and timing.
Images come from NTB's preview server over the internet, so image timings vary with the network. Compare counts and bytes first, and times as medians.
Chrome DevTools MCP
Configured for this repo in .mcp.json: chrome-devtools-mcp 1.10.1 (pinned), launching its own Chrome with a temporary profile (--isolated), 1440 × 900, without sending URLs to CrUX and without usage statistics. Claude Code asks once to approve the project's MCP server, and loads it at the start of a session. If its tools don't appear, check /mcp: a rejected approval is remembered in .claude/settings.local.json (disabledMcpjsonServers). Use its emulate tool for CPU slowdown (4×, as the script) before tracing, or a fast machine hides the problems.
Useful requests to Claude:
- "Record a performance trace of http://localhost:3300/features/image-grid/app?total=2000 while scrolling, and explain the long animation frames."
- "Show the network waterfall for the first screen of the image grid. What delays LCP?"
- "Emulate a slow 4G connection and a 4× slower CPU, and compare LCP for both engines."