diff --git a/.claude/agents/profiler.md b/.claude/agents/profiler.md
index a354a7f2..214949dc 100644
--- a/.claude/agents/profiler.md
+++ b/.claude/agents/profiler.md
@@ -17,7 +17,9 @@ You receive from the parent agent:
## How It Works
-The app has `react-scan` configured with `report: true` in dev mode (`src/lib/react-scan.ts`). It exposes `window.__getReactScanReport()` which returns per-component render counts and times: `{ ComponentName: { count, time } }`.
+`src/lib/react-scan.ts` runs react-scan in dev mode and accumulates render data via its `onRender` option. It exposes `window.__getReactScanReport()`, which returns a plain, JSON-serializable object of per-component render counts and times: `{ ComponentName: { count, time } }`, plus `window.__resetReactScanReport()` to zero it between phases.
+
+Do NOT call react-scan's own `getReport()` — in 0.5.3 it reads a Map that is never written to, and a `Map` stringifies to `"{}"` anyway.
The profiler's `addInitScript` also intercepts `__REACT_DEVTOOLS_GLOBAL_HOOK__` to count React commits independently (works even if react-scan is not loaded).
@@ -163,7 +165,7 @@ Routes profiled: /route1, /route2, ...
- Always use the `-s=SESSION` flag on every playwright-cli command
- Replace `SESSION` and `ROUTE` placeholders with actual values
- **Collect per-route data before navigating to the next route** — goto resets the document
-- If `__getReactScanReport` returns null, note "react-scan report unavailable" and rely on commit counts
+- If `__getReactScanReport` is undefined or returns `{}`, wait ~1s and retry once (it is a dynamic import); if still empty, note "react-scan report unavailable" and rely on commit counts
- If a route has no content or fails to load, note it in Info and move on
- **Always stop tracing and close the browser when done, even on errors** — wrap your workflow in a try/finally mindset: if any step fails, still run `tracing-stop` and `close`
- Board codes (`biz`, `pol`, `g`, etc.) map to community addresses via the app's directory
diff --git a/.claude/skills/profile-browsing/SKILL.md b/.claude/skills/profile-browsing/SKILL.md
index 1fc6821e..244c7246 100644
--- a/.claude/skills/profile-browsing/SKILL.md
+++ b/.claude/skills/profile-browsing/SKILL.md
@@ -16,10 +16,14 @@ Two-layer profiling: browser-level symptoms (Web Vitals, long tasks, scroll jank
### react-scan (already configured)
-The app has `react-scan` set up in `src/lib/react-scan.ts` with `report: true`. In dev mode it:
+`src/lib/react-scan.ts` runs react-scan in dev mode. It:
- Highlights rerendering components visually (toolbar + overlay)
-- Tracks per-component render counts and times internally
-- Exposes `window.__getReactScanReport()` for programmatic collection
+- Accumulates per-component render counts and times via react-scan's `onRender` option
+- Exposes `window.__getReactScanReport()` and `window.__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.
@@ -160,4 +164,4 @@ playwright-cli -s=prof-3 close 2>/dev/null
- **addInitScript persistence**: Instrumentation re-injects automatically in each new document.
- **Tracing**: Each subagent produces a `trace.zip` viewable in [Trace Viewer](https://trace.playwright.dev).
- **Board codes**: `biz`, `pol`, `g`, `a`, `v`, etc. map to community addresses via the directory.
-- **Without react-scan**: If `__getReactScanReport` returns null, the profiler falls back to commit counts + render bursts (still useful, just no component names).
+- **Empty react-scan report**: react-scan is a dynamic import, so `__getReactScanReport()` returns `{}` for the first moment after a `goto`. If it is empty, wait ~1s and re-read before falling back to commit counts + render bursts (still useful, just no component names).
diff --git a/.codex/skills/profile-browsing/SKILL.md b/.codex/skills/profile-browsing/SKILL.md
index 3927c608..224545fc 100644
--- a/.codex/skills/profile-browsing/SKILL.md
+++ b/.codex/skills/profile-browsing/SKILL.md
@@ -16,10 +16,14 @@ Two-layer profiling: browser-level symptoms (Web Vitals, long tasks, scroll jank
### react-scan (already configured)
-The app has `react-scan` set up in `src/lib/react-scan.ts` with `report: true`. In dev mode it:
+`src/lib/react-scan.ts` runs react-scan in dev mode. It:
- Highlights rerendering components visually (toolbar + overlay)
-- Tracks per-component render counts and times internally
-- Exposes `window.__getReactScanReport()` for programmatic collection
+- Accumulates per-component render counts and times via react-scan's `onRender` option
+- Exposes `window.__getReactScanReport()` and `window.__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.
@@ -160,4 +164,4 @@ playwright-cli -s=prof-3 close 2>/dev/null
- **addInitScript persistence**: Instrumentation re-injects automatically in each new document.
- **Tracing**: Each subagent produces a `trace.zip` viewable in [Trace Viewer](https://trace.playwright.dev).
- **Board codes**: `biz`, `pol`, `g`, `a`, `v`, etc. map to community addresses via the directory.
-- **Without react-scan**: If `__getReactScanReport` returns null, the profiler falls back to commit counts + render bursts (still useful, just no component names).
+- **Empty react-scan report**: react-scan is a dynamic import, so `__getReactScanReport()` returns `{}` for the first moment after a `goto`. If it is empty, wait ~1s and re-read before falling back to commit counts + render bursts (still useful, just no component names).
diff --git a/.cursor/agents/profiler.md b/.cursor/agents/profiler.md
index c4da66c5..ab4a96a2 100644
--- a/.cursor/agents/profiler.md
+++ b/.cursor/agents/profiler.md
@@ -17,7 +17,9 @@ You receive from the parent agent:
## How It Works
-The app has `react-scan` configured with `report: true` in dev mode (`src/lib/react-scan.ts`). It exposes `window.__getReactScanReport()` which returns per-component render counts and times: `{ ComponentName: { count, time } }`.
+`src/lib/react-scan.ts` runs react-scan in dev mode and accumulates render data via its `onRender` option. It exposes `window.__getReactScanReport()`, which returns a plain, JSON-serializable object of per-component render counts and times: `{ ComponentName: { count, time } }`, plus `window.__resetReactScanReport()` to zero it between phases.
+
+Do NOT call react-scan's own `getReport()` — in 0.5.3 it reads a Map that is never written to, and a `Map` stringifies to `"{}"` anyway.
The profiler's `addInitScript` also intercepts `__REACT_DEVTOOLS_GLOBAL_HOOK__` to count React commits independently (works even if react-scan is not loaded).
@@ -163,7 +165,7 @@ Routes profiled: /route1, /route2, ...
- Always use the `-s=SESSION` flag on every playwright-cli command
- Replace `SESSION` and `ROUTE` placeholders with actual values
- **Collect per-route data before navigating to the next route** — goto resets the document
-- If `__getReactScanReport` returns null, note "react-scan report unavailable" and rely on commit counts
+- If `__getReactScanReport` is undefined or returns `{}`, wait ~1s and retry once (it is a dynamic import); if still empty, note "react-scan report unavailable" and rely on commit counts
- If a route has no content or fails to load, note it in Info and move on
- **Always stop tracing and close the browser when done, even on errors** — wrap your workflow in a try/finally mindset: if any step fails, still run `tracing-stop` and `close`
- Board codes (`biz`, `pol`, `g`, etc.) map to community addresses via the app's directory
diff --git a/.cursor/skills/profile-browsing/SKILL.md b/.cursor/skills/profile-browsing/SKILL.md
index e7047969..8f10c1a0 100644
--- a/.cursor/skills/profile-browsing/SKILL.md
+++ b/.cursor/skills/profile-browsing/SKILL.md
@@ -16,10 +16,14 @@ Two-layer profiling: browser-level symptoms (Web Vitals, long tasks, scroll jank
### react-scan (already configured)
-The app has `react-scan` set up in `src/lib/react-scan.ts` with `report: true`. In dev mode it:
+`src/lib/react-scan.ts` runs react-scan in dev mode. It:
- Highlights rerendering components visually (toolbar + overlay)
-- Tracks per-component render counts and times internally
-- Exposes `window.__getReactScanReport()` for programmatic collection
+- Accumulates per-component render counts and times via react-scan's `onRender` option
+- Exposes `window.__getReactScanReport()` and `window.__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.
@@ -160,4 +164,4 @@ playwright-cli -s=prof-3 close 2>/dev/null
- **addInitScript persistence**: Instrumentation re-injects automatically in each new document.
- **Tracing**: Each subagent produces a `trace.zip` viewable in [Trace Viewer](https://trace.playwright.dev).
- **Board codes**: `biz`, `pol`, `g`, `a`, `v`, etc. map to community addresses via the directory.
-- **Without react-scan**: If `__getReactScanReport` returns null, the profiler falls back to commit counts + render bursts (still useful, just no component names).
+- **Empty react-scan report**: react-scan is a dynamic import, so `__getReactScanReport()` returns `{}` for the first moment after a `goto`. If it is empty, wait ~1s and re-read before falling back to commit counts + render bursts (still useful, just no component names).
diff --git a/docs/agent-playbooks/known-surprises.md b/docs/agent-playbooks/known-surprises.md
index a5981554..219fd49c 100644
--- a/docs/agent-playbooks/known-surprises.md
+++ b/docs/agent-playbooks/known-surprises.md
@@ -158,3 +158,13 @@ If uncertain, ask the developer before adding an entry.
- **Impact:** Notarization aborts after a successful signing pass; the error message looks like a signing failure and invites debugging the certificate/keychain instead of the real cause. Any tool that shells out to `codesign` with a bare relative path can hit this because the app is literally named `5chan`.
- **Mitigation:** Keep the yarn patch `.yarn/patches/@electron-notarize-npm-2.5.0-*.patch` (backport of electron/notarize#245, prefixes the basename with `./`) until electron-forge depends on `@electron/notarize` >= 3.x. When invoking `codesign` manually on the app bundle, always use an absolute or `./`-prefixed path.
- **Status:** confirmed
+
+### react-scan's `getReport()` is dead API and can never return data
+
+- **Date:** 2026-07-27
+- **Observed by:** contributor + Claude
+- **Context:** Running the `profile-browsing` skill against a branch to measure excessive rerenders, and getting no component data back
+- **What was surprising:** Three independent failures stacked up silently. (1) `getReport()` returns `Store.legacyReportData`, which react-scan 0.5.3 initializes as an empty `Map` and never writes to anywhere in the bundle. (2) The live `Store.reportData` is only populated inside `if (options.showToolbar !== false && Store.inspectState.value.kind === 'focused')` — but the profiler sets `__PROFILING__=true`, which sets `showToolbar: false`, and `'focused'` requires a human clicking the inspector onto one component, so it is unreachable under automation. (3) `getReport()` returns a `Map`, and `JSON.stringify(new Map())` is `"{}"` regardless of contents, so the skill's collection line would have printed `{}` even if data existed. The skill and profiler agent additionally claimed the app was configured with `report: true`; react-scan 0.5.3 has no `report` option at all, and passing one logs `[React Scan] Invalid options: - Unknown option "report"`.
+- **Impact:** Every profiling run reported zero react-scan component data without erroring, so rerender hotspots looked invisible and profiling silently degraded to raw commit counts.
+- **Mitigation:** `src/lib/react-scan.ts` now accumulates render data through react-scan's `onRender` option, which is only skipped when `isPaused && inspectorInactive` (verified `isPaused: false` with the toolbar off). It exposes `window.__getReactScanReport()` returning a plain, JSON-serializable object and `window.__resetReactScanReport()`. Never reintroduce `getReport()`, and never `JSON.stringify` a `Map`. Set `window.__PROFILING_UNNECESSARY__ = true` to opt into `trackUnnecessaryRenders`; it is off by default because it adds overhead that skews the `time` field.
+- **Status:** confirmed
diff --git a/docs/agent-runs/excessive-rerenders/feature-list.json b/docs/agent-runs/excessive-rerenders/feature-list.json
new file mode 100644
index 00000000..f9188f8c
--- /dev/null
+++ b/docs/agent-runs/excessive-rerenders/feature-list.json
@@ -0,0 +1,118 @@
+{
+ "task": "excessive-rerenders",
+ "last_updated": "2026-07-30",
+ "items": [
+ {
+ "id": "F001",
+ "priority": 1,
+ "status": "verified",
+ "description": "Add serializable React profiling and establish route baselines for /biz, /all, /all/catalog, and the homepage.",
+ "verification": [
+ "Profile fresh isolated Chromium sessions with window.__getReactScanReport().",
+ "Confirm the branch-scoped Portless URL is served by exactly one dev server."
+ ],
+ "files": [
+ "src/lib/react-scan.ts"
+ ],
+ "notes": "Committed as 0f0b6118c. Baselines captured before optimization."
+ },
+ {
+ "id": "F002",
+ "priority": 2,
+ "status": "verified",
+ "description": "Narrow routing, directory, feed, post, shell, and homepage state subscriptions so unchanged lifecycle ticks do not rerender unrelated UI.",
+ "verification": [
+ "yarn test",
+ "yarn lint",
+ "yarn type-check",
+ "yarn build",
+ "yarn doctor --scope changed --base master"
+ ],
+ "files": [],
+ "notes": "Implemented incrementally in commits 912e1bf00 through 066e64a7d."
+ },
+ {
+ "id": "F003",
+ "priority": 3,
+ "status": "verified",
+ "description": "Derive multi-community loading text from primitive store selectors so hidden and visible feed footers stop rerendering on unchanged state.",
+ "verification": [
+ "yarn test",
+ "yarn lint",
+ "yarn type-check",
+ "yarn build",
+ "Fresh /all profile reaches the first post and records an untouched idle sample.",
+ "Fresh /all/catalog plus back/forward checks preserve ordering, anchoring, scroll restoration, and mobile width."
+ ],
+ "files": [
+ "src/hooks/use-state-string.ts",
+ "src/hooks/__tests__/use-state-string.test.tsx",
+ "src/lib/bitsocial-internals/utils.ts"
+ ],
+ "notes": "Committed as a72054e5d. /all first post improved from 42.21s to 3.88s; cached /all idle improved from 260 commits/5.03s to zero."
+ },
+ {
+ "id": "F004",
+ "priority": 4,
+ "status": "verified",
+ "description": "Stop resolved homepage community-stat collectors from subscribing to full community lifecycle objects while preserving stat refresh when statsCid changes.",
+ "verification": [
+ "Add focused collector lifecycle regression tests.",
+ "Profile the homepage until community stats have loaded, then collect an untouched idle sample.",
+ "Run yarn test, yarn lint, yarn type-check, yarn build, and scoped React Doctor."
+ ],
+ "files": [
+ "src/hooks/use-communities-stats.ts",
+ "src/hooks/__tests__/use-communities-stats.test.ts"
+ ],
+ "notes": "Committed as d6f2fa8b6. With 64 boards displayed, the loaded homepage improved from 235-260 commits per two seconds to zero commits across five seconds."
+ },
+ {
+ "id": "F005",
+ "priority": 5,
+ "status": "verified",
+ "description": "Run final advisory reviews, low-spec Chromium verification, cross-browser UI checks, and summarize remaining non-regression performance opportunities.",
+ "verification": [
+ "Run both React best-practice reviews and the effect review for new hook usage.",
+ "Run the code-quality-review skill.",
+ "Run a throttled Chromium pass plus desktop and mobile Firefox/WebKit verification.",
+ "Confirm a clean worktree and incremental local commits."
+ ],
+ "files": [],
+ "notes": "React and effect reviews found no new issue. Code-quality review found no high-confidence advisory finding. Mid-tier throttled Chromium plus current Firefox/WebKit desktop and mobile checks passed without an app exception or horizontal overflow."
+ },
+ {
+ "id": "F006",
+ "priority": 6,
+ "status": "verified",
+ "description": "Reduce homepage acquisition-phase React commits while decentralized community stats resolve, without changing the displayed results.",
+ "verification": [
+ "Capture a fresh homepage acquisition baseline with component and commit timings.",
+ "Add focused regression tests for any subscription or batching change.",
+ "Reprofile through the loaded-board condition, compare the first twenty seconds, and record an untouched idle sample."
+ ],
+ "files": [
+ "src/hooks/use-communities-stats.ts",
+ "src/hooks/__tests__/use-communities-stats.test.ts",
+ "src/views/home/home.tsx",
+ "src/views/home/__tests__/home.test.tsx"
+ ],
+ "notes": "Committed as 28fda1ce3. First-twenty-second commits improved from 1,211 to 862 (-29%), and CommunityStatsRequest renders from 1,569 to 491 (-69%). All 64 boards resolved at 21.06s, about one second after the baseline run, then the next five seconds recorded zero commits."
+ },
+ {
+ "id": "F007",
+ "priority": 7,
+ "status": "verified",
+ "description": "Measure representative production cold startup under constrained CPU and network, then reduce only verified application-owned bottlenecks.",
+ "verification": [
+ "Separate Vite development-module overhead from a production build served locally.",
+ "Record production navigation, LCP, long-task, transfer, and bundle evidence under the mid-tier throttle.",
+ "Rebuild and remeasure after any retained optimization."
+ ],
+ "files": [
+ "vite.config.js"
+ ],
+ "notes": "Committed as 0de61ac56. In a Brotli production build with the service worker blocked for a controlled A/B, first content improved from 8.31s to 4.66s, LCP from 8.37s to 4.72s, DOMContentLoaded from 4.97s to 1.07s, and long-task time from 1.85s to 1.51s. The PWA precache fell from 4.06 MiB to 439.45 KiB. Production Chrome, Firefox, and WebKit retained the full board list, active stats resolution, mobile width, and no app error boundary."
+ }
+ ]
+}
diff --git a/docs/agent-runs/excessive-rerenders/progress.md b/docs/agent-runs/excessive-rerenders/progress.md
new file mode 100644
index 00000000..ec5fb83d
--- /dev/null
+++ b/docs/agent-runs/excessive-rerenders/progress.md
@@ -0,0 +1,39 @@
+# Progress Log
+
+## 2026-07-30 00:04 HKT
+
+- Item: F003
+- Summary: Resumed after the permission interruption and completed controlled post-fix `/all` and `/all/catalog` profiling. The selector-scoped loading text fix removes the hidden and visible footer loops while preserving feed ordering, scroll anchoring, exact back/forward restoration, mobile width, and search interaction.
+- Files: `src/hooks/use-state-string.ts`, `src/hooks/__tests__/use-state-string.test.tsx`, `src/lib/bitsocial-internals/utils.ts`
+- Verification: `./scripts/agent-init.sh --smoke`; fresh Chromium `/all` load and five-second idle profile; `/all` ordering and 15-second anchor check; fresh `/all/catalog` idle and cached back/forward profile; desktop and mobile Firefox/WebKit smoke checks
+- Blockers: none
+- Next: Complete F004 by making homepage community-stat collectors dormant after resolving the current `statsCid`, then reprofile the loaded homepage.
+
+## 2026-07-30 00:17 HKT
+
+- Item: F004, F005
+- Summary: Split homepage stat collection into a CID-selecting wrapper and a request component that unmounts after the current stats CID resolves. A changed CID remounts the request and ignores the previous CID's cached result until the data hook publishes a fresh object. The 64-board homepage is now fully idle instead of continuously committing collector updates.
+- Files: `src/hooks/use-communities-stats.ts`, `src/hooks/__tests__/use-communities-stats.test.ts`
+- Verification: `corepack yarn test --configLoader runner`; `corepack yarn lint`; `corepack yarn type-check`; `corepack yarn build`; `corepack yarn doctor --scope changed --base master`; React best-practice and effect reviews; code-quality review; fresh Chromium homepage profiling; mid-tier throttled Chromium homepage and `/all`; desktop and 390px mobile Firefox/WebKit homepage smoke checks
+- Blockers: none
+- Next: Keep acquisition-phase decentralized provider work and cold bundle loading as separate follow-up opportunities; this task's excessive steady-state rerender loops are resolved.
+
+## 2026-07-30 01:06 HKT
+
+- Item: F006
+- Summary: Added one shared homepage metadata loader and held each stats request until its stable stats CID exists. This removes the pre-CID request lifecycles and key-driven restarts while preserving refreshes when a CID changes.
+- Files: `src/hooks/use-communities-stats.ts`, `src/hooks/__tests__/use-communities-stats.test.ts`, `src/views/home/home.tsx`, `src/views/home/__tests__/home.test.tsx`
+- Verification: focused tests; full 1,374-test suite; lint; type-check; build; scoped React Doctor; React best-practice, effect, and code-quality reviews; fresh acquisition profiles; desktop and mobile Chrome, Firefox, and WebKit homepage checks
+- Result: First-twenty-second commits improved from 1,211 to 862 (-29%), CommunityStatsRequest renders improved from 1,569 to 491 (-69%), all 64 boards resolved at 21.06s, and the following five seconds were commit-free.
+- Blockers: none
+- Next: Establish a production-build cold-start baseline before changing startup code.
+
+## 2026-07-30 01:06 HKT
+
+- Item: F007
+- Summary: Removed the forced whole-package protocol chunk so the browser can render the homepage from the smaller hook/core graph and load deeper decentralized protocol modules on demand.
+- Files: `vite.config.js`
+- Verification: controlled Brotli production-build A/B with service workers blocked and identical 4x CPU plus mid-tier network throttling; final production build; lint; type-check; production desktop and mobile checks in Chrome, Firefox, and WebKit
+- Result: First content improved from 8.31s to 4.66s, LCP from 8.37s to 4.72s, DOMContentLoaded from 4.97s to 1.07s, load from 5.63s to 1.83s, long-task time from 1.85s to 1.51s, and PWA precache from 4.06 MiB to 439.45 KiB. The full board list remains present while decentralized stats continue resolving.
+- Blockers: none
+- Next: None; F001-F007 are verified.
diff --git a/public/llms-full.txt b/public/llms-full.txt
index c57cdf0d..199567c5 100644
--- a/public/llms-full.txt
+++ b/public/llms-full.txt
@@ -974,6 +974,16 @@ If uncertain, ask the developer before adding an entry.
- **Impact:** Notarization aborts after a successful signing pass; the error message looks like a signing failure and invites debugging the certificate/keychain instead of the real cause. Any tool that shells out to `codesign` with a bare relative path can hit this because the app is literally named `5chan`.
- **Mitigation:** Keep the yarn patch `.yarn/patches/@electron-notarize-npm-2.5.0-*.patch` (backport of electron/notarize#245, prefixes the basename with `./`) until electron-forge depends on `@electron/notarize` >= 3.x. When invoking `codesign` manually on the app bundle, always use an absolute or `./`-prefixed path.
- **Status:** confirmed
+
+### react-scan's `getReport()` is dead API and can never return data
+
+- **Date:** 2026-07-27
+- **Observed by:** contributor + Claude
+- **Context:** Running the `profile-browsing` skill against a branch to measure excessive rerenders, and getting no component data back
+- **What was surprising:** Three independent failures stacked up silently. (1) `getReport()` returns `Store.legacyReportData`, which react-scan 0.5.3 initializes as an empty `Map` and never writes to anywhere in the bundle. (2) The live `Store.reportData` is only populated inside `if (options.showToolbar !== false && Store.inspectState.value.kind === 'focused')` — but the profiler sets `__PROFILING__=true`, which sets `showToolbar: false`, and `'focused'` requires a human clicking the inspector onto one component, so it is unreachable under automation. (3) `getReport()` returns a `Map`, and `JSON.stringify(new Map())` is `"{}"` regardless of contents, so the skill's collection line would have printed `{}` even if data existed. The skill and profiler agent additionally claimed the app was configured with `report: true`; react-scan 0.5.3 has no `report` option at all, and passing one logs `[React Scan] Invalid options: - Unknown option "report"`.
+- **Impact:** Every profiling run reported zero react-scan component data without erroring, so rerender hotspots looked invisible and profiling silently degraded to raw commit counts.
+- **Mitigation:** `src/lib/react-scan.ts` now accumulates render data through react-scan's `onRender` option, which is only skipped when `isPaused && inspectorInactive` (verified `isPaused: false` with the toolbar off). It exposes `window.__getReactScanReport()` returning a plain, JSON-serializable object and `window.__resetReactScanReport()`. Never reintroduce `getReport()`, and never `JSON.stringify` a `Map`. Set `window.__PROFILING_UNNECESSARY__ = true` to opt into `trackUnnecessaryRenders`; it is off by default because it adds overhead that skews the `time` field.
+- **Status:** confirmed
```
---
diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx
index 6ceeea36..dee97aed 100644
--- a/src/components/board-header/board-header.tsx
+++ b/src/components/board-header/board-header.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams, useNavigate } from 'react-router-dom';
import { useAccountComment, useCommunity } from '@bitsocial/bitsocial-react-hooks';
@@ -44,7 +44,8 @@ const OfflineIndicator = ({ communityAddress }: { communityAddress: string | und
);
};
-const BoardHeader = () => {
+// No props, so parent rerenders never change its output.
+const BoardHeader = memo(() => {
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
@@ -136,6 +137,7 @@ const BoardHeader = () => {
{!isInArchiveView &&