Playwright disables normal background throttling, so a hidden 5chan page keeps doing P2P and rendering work after a check finishes. Agents verifying in parallel across worktrees stacked whole browser engines on one machine. Add scripts/pw-session.sh, a wrapper that permits one active Playwright browser at a time and records who holds it: - The lock is machine-wide, not per-repository, because the contended resource is RAM and CPU. Every worktree and checkout shares one slot. - Acquisition is an atomic mkdir. Stale locks clear themselves: `open` reclaims any slot whose recorded browser is no longer `status: open` in `playwright-cli list --all`, so an interrupted workflow cannot strand the budget. When that list cannot be read the lock is left alone, so a broken CLI never silently disables the budget. - `open` exits 75 when the slot is busy; `--wait[=SECONDS]` blocks instead. - `close` always stops the browser, even when the lock was already lost, and never releases a slot held by a different session. - `status` reports the holder and whether its browser is still alive. Agent policy now runs browser engines and profiler batches sequentially, uses Chrome/Blink during iteration and the full engine matrix only for final verification, and never uses `close-all` or `kill-all` while other agents may own sessions. Covered by scripts/pw-session.test.js.
8.4 KiB
name, description
| name | description |
|---|---|
| profile-browsing | Profile app performance while browsing, collecting Web Vitals and React rerender data via react-scan. Orchestrates sequential profiler subagents via playwright-cli to capture navigation timing, long tasks, layout shifts, LCP, React commit counts, render bursts, and per-component render data without saturating the machine. Use when profiling browsing performance, finding bottlenecks, diagnosing excessive rerenders, or auditing page performance. |
Profile Browsing Performance
Two-layer profiling: browser-level symptoms (Web Vitals, long tasks, scroll jank) and React-level diagnosis (commit counts, render bursts, per-component render data from react-scan). Each profiler subagent runs in its own browser session and context window, with only one profiler active at a time.
Prerequisites
- Dev server running at https://5chan.localhost (
yarn startvia Portless) playwright-cliinstalled (npm install -g @playwright/cli@latest)
IMPORTANT: The orchestrator (you) is responsible for ensuring exactly ONE dev server is running. Profiler subagents must NEVER start a dev server themselves.
react-scan (already configured)
src/lib/react-scan.ts runs react-scan in dev mode. It:
- Highlights rerendering components visually (toolbar + overlay)
- Accumulates per-component render counts and times via react-scan's
onRenderoption - Exposes
window.__getReactScanReport()andwindow.__resetReactScanReport()for programmatic collection
__getReactScanReport() returns a plain object: { ComponentName: { count, time } }.
Do not use react-scan's own getReport(). It reads Store.legacyReportData, which react-scan 0.5.3 never writes to, so it always returns an empty Map. The live Store.reportData is no better: it is only populated while the toolbar is visible and a component is manually focused in the inspector, neither of which holds under automation. The app's onRender collector exists precisely because of this. Also note a Map cannot be serialized — JSON.stringify(new Map()) is "{}" regardless of contents — which is why the collector returns a plain object.
The profiler's addInitScript sets window.__PROFILING__ = true before the app loads, which tells react-scan to disable its toolbar and sounds during automated runs.
No additional setup needed — react-scan is already a devDependency and imported in the entry file.
Step 0: Ensure Dev Server is Running
Before running any profiler subagents, verify exactly one dev server is available:
# Check if the dev server is reachable
curl -sf https://5chan.localhost -o /dev/null && echo "OK" || echo "NOT RUNNING"
- If OK: proceed to Step 1.
- If NOT RUNNING: start one instance with
yarn start(backgrounded), then poll until it responds. Do NOT start more than one. - If a dev server is already running on a different port (check
ps aux | grep vite), reuse it — do not start another.
Step 1: Define Route Batches
Split routes into batches of 2–4 for sequential profiling. Give every batch a short task-specific session name so unrelated profiling runs cannot collide.
Default batches (adjust boards as needed):
| Batch | Session | Routes | Focus |
|---|---|---|---|
| 1 | prof-1 |
/all, /all/catalog |
Multi-board feed + catalog |
| 2 | prof-2 |
/biz, /biz/catalog |
Single board feed + catalog |
| 3 | prof-3 |
/pol, /pol/catalog, /g, /g/catalog |
Board switching (feed reloads) |
Keep batches balanced. Add thread views (/:boardIdentifier/thread/:cid) as needed.
Step 2: Run Profiler Subagents Sequentially
Read the profiler subagent definition at .cursor/agents/profiler.md. Then spawn one profiler Task for the first batch:
For each batch, create a Task:
subagent_type: "profiler"
prompt: |
Session name: "prof-N"
Routes to profile: /route1, /route2, ...
Any non-default app URL or extra profiling constraints
Wait for that profiler to close its browser and return results before spawning the next batch. Never run profiler or browser-check subagents concurrently: competing browser sessions both saturate the machine and invalidate timing measurements.
Step 3: Merge Results
Collect structured output from each subagent and merge:
- Concatenate all Critical / Warning / React Rerenders / Scroll Jank / Info items
- Combine per-view summary tables into one
- Merge react-scan component data across routes (same component appearing in multiple routes = sum counts)
- Deduplicate shared issues (e.g., same slow resource across routes)
- Sort by severity (Critical first)
Step 4: Final Report
## Performance Profile Results
### Critical
- [metric]: [value] at [route] — [what likely needs fixing]
### Warning
- [metric]: [value] at [route] — [what likely needs fixing]
### React Rerenders
- [route]: [N] commits during load, [M] during scroll — [likely cause]
- Render bursts detected at [routes] — suggests cascading state updates
- Top rerendering components (react-scan):
- [ComponentName]: [total count] renders across [routes], [time]ms total
- [ComponentName]: [total count] renders across [routes], [time]ms total
### Scroll Jank
- [route]: [N] long tasks during scroll (max [X]ms), [M] React commits — [likely cause]
### Info
- [observations]
### Per-View Summary
| View | Nav (ms) | Long Tasks | CLS | LCP (ms) | Commits | Scroll Commits | Bursts | Top Component |
|------|----------|-----------|-----|-----------|---------|----------------|--------|---------------|
| /all | ... | ... | ... | ... | ... | ... | ... | ... |
Interpreting React Metrics
| Signal | Likely cause | Fix direction |
|---|---|---|
| High commits, no long tasks | Frequent cheap rerenders | React.memo, stabilize props |
| High commits + long tasks | Expensive rerenders | Profile render cost, split components |
| High scroll commits | Scroll/intersection observer triggering renders | Throttle handlers, memoize list items |
| Render bursts (>5 in 100ms) | Cascading state updates | Batch updates, review Zustand selectors |
| react-scan: component with >30 renders | Missing memoization or unstable references | useMemo/useCallback, check parent renders |
| react-scan: component with >50ms time | Expensive render function | Split component, move work out of render |
Element-source follow-up
When react-scan identifies a rerender hotspot but you still need the exact file behind a concrete DOM node, hand off to $inspect-elements.
playwright-cli -s=prof-followup eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7
Use source.filePath as the direct edit target and stack to understand which parent components own the node.
Step 5: Cleanup
After profiling is complete and the report is delivered, verify no orphaned processes were left behind:
# Check for any Vite dev servers started during profiling
ps aux | grep 'vite.*--port' | grep -v grep
- If the dev server was already running before Step 0, leave it alone.
- If the orchestrator started the dev server in Step 0, kill it now.
- If there are multiple Vite processes (should never happen), kill the extras and warn the user.
Confirm the profiling session released the shared browser slot:
./scripts/pw-session.sh status
If a failed profiler still owns the slot, close that exact recorded session with ./scripts/pw-session.sh close <session>. A slot whose browser already died is reclaimed by the next open, so it needs no manual cleanup. Never use close-all or kill-all during concurrent agent work.
Notes
- Session isolation: Each subagent uses a short task-specific playwright-cli session (
-s=prof-<task>-N). - Context isolation: Each subagent runs in its own context window.
- Per-route collection: Data resets on each
goto— the profiler collects before navigating away. - addInitScript persistence: Instrumentation re-injects automatically in each new document.
- Tracing: Each subagent produces a
trace.zipviewable in Trace Viewer. - Board codes:
biz,pol,g,a,v, etc. map to community addresses via the directory. - Empty react-scan report: react-scan is a dynamic import, so
__getReactScanReport()returns{}for the first moment after agoto. If it is empty, wait ~1s and re-read before falling back to commit counts + render bursts (still useful, just no component names).