diff --git a/features/index.html b/features/index.html index f12a4ce..d3bf498 100644 --- a/features/index.html +++ b/features/index.html @@ -58,7 +58,8 @@ {"@type": "ListItem", "position": 3, "name": "Obsidian wiki for token-efficient context"}, {"@type": "ListItem", "position": 4, "name": "Durable state on disk"}, {"@type": "ListItem", "position": 5, "name": "Automated CI maintenance"}, - {"@type": "ListItem", "position": 6, "name": "Modern React stack"} + {"@type": "ListItem", "position": 6, "name": "Modern React stack"}, + {"@type": "ListItem", "position": 7, "name": "React render-performance diagnostics"} ] }, { @@ -113,12 +114,20 @@ "text": "Run /gaia-fitness. It audits your entire Claude integration, including hooks, rules, skills, CLAUDE.md, settings, and GAIA install fitness, then grades each area A+ to F and heals what it can on a branch. You review the diff and commit when satisfied. The check protocol survives /update-gaia." } }, + { + "@type": "Question", + "name": "How does GAIA diagnose slow React renders?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Run /gaia-react-perf. It drives a micro-interaction you point it at, captures real per-render attribution with a bippy harness, and reduces the raw capture to a small ranked summary so it never enters Claude's context. It flags a React.memo that re-renders anyway because a prop or context value is a new object or callback every render, plus any component whose render crosses a 16 ms frame budget. It ranks findings by how costly each wasted render is and how widely it ripples, then names the structural fix. v1 is measure-only: GAIA diagnoses, Claude applies the fix you approve." + } + }, { "@type": "Question", "name": "How does GAIA automate project maintenance?", "acceptedAnswer": { "@type": "Answer", - "text": "GAIA CI handles dependency updates (patch and minor auto-merge with codemods, major routes to a review PR), daily security audits with pnpm audit, wiki sync whenever app code changes, and monthly stale branch cleanup. Claude handles the safe cases automatically. Humans only see what it cannot safely recover from, with a hard $5 cost cap per run." + "text": "GAIA CI handles dependency updates (patch and minor auto-merge with codemods, major routes to a review PR), daily security audits with pnpm audit, wiki sync whenever app code changes, and monthly stale branch cleanup. Claude handles the safe cases automatically. Humans only see what it cannot safely recover from. GAIA CI runs against your Claude Code Pro/Max subscription or your Anthropic API key, and a fixed turn limit caps every scheduled run, so no run loops without end." } }, { diff --git a/index.html b/index.html index cbc2b0f..64b7bd2 100644 --- a/index.html +++ b/index.html @@ -110,6 +110,7 @@ "Obsidian wiki for token-efficient context management", "Automated GAIA CI with code-review audit", "Token economics controls and trust scaffolding", + "React render-performance diagnostics via /gaia-react-perf", "One-command React 19 project scaffolding via create-gaia" ], "offers": { diff --git a/src/pages/consulting/sections/Consulting.tsx b/src/pages/consulting/sections/Consulting.tsx index e9b2e6c..dd9aa01 100644 --- a/src/pages/consulting/sections/Consulting.tsx +++ b/src/pages/consulting/sections/Consulting.tsx @@ -772,7 +772,7 @@ const Consulting = () => { {SKUS.map((sku) => (
diff --git a/src/pages/features/App.tsx b/src/pages/features/App.tsx index 9c38231..ea9674f 100644 --- a/src/pages/features/App.tsx +++ b/src/pages/features/App.tsx @@ -10,6 +10,7 @@ import Hero from './sections/Hero'; import LoadOnDemand from './sections/LoadOnDemand'; import ObsidianWikiDetail from './sections/ObsidianWikiDetail'; import Principles from './sections/Principles'; +import ReactPerf from './sections/ReactPerf'; import Stack from './sections/Stack'; import StateOnDisk from './sections/StateOnDisk'; import Trust from './sections/Trust'; @@ -23,6 +24,7 @@ const App = () => { + diff --git a/src/pages/features/sections/AgenticDesignDetail.tsx b/src/pages/features/sections/AgenticDesignDetail.tsx index 5a73a2f..580cfba 100644 --- a/src/pages/features/sections/AgenticDesignDetail.tsx +++ b/src/pages/features/sections/AgenticDesignDetail.tsx @@ -88,7 +88,7 @@ const pad = (n: number) => String(n + 1).padStart(2, '0'); const AgenticDesignDetail = () => (
diff --git a/src/pages/features/sections/Fitness.tsx b/src/pages/features/sections/Fitness.tsx index c026471..fdc4f2e 100644 --- a/src/pages/features/sections/Fitness.tsx +++ b/src/pages/features/sections/Fitness.tsx @@ -93,7 +93,7 @@ const gradeTone = (grade: string) => { const Fitness = () => (
diff --git a/src/pages/features/sections/Forensics.tsx b/src/pages/features/sections/Forensics.tsx index 62af6a3..9dd726e 100644 --- a/src/pages/features/sections/Forensics.tsx +++ b/src/pages/features/sections/Forensics.tsx @@ -2,7 +2,7 @@ import ForensicsIssue from './figures/ForensicsIssue'; const Forensics = () => (
diff --git a/src/pages/features/sections/FxSection.tsx b/src/pages/features/sections/FxSection.tsx index 523b34a..3277708 100644 --- a/src/pages/features/sections/FxSection.tsx +++ b/src/pages/features/sections/FxSection.tsx @@ -18,7 +18,7 @@ const FxSection = ({ title, }: Properties) => (
diff --git a/src/pages/features/sections/GaiaCi.tsx b/src/pages/features/sections/GaiaCi.tsx index bc51a13..dafbd5d 100644 --- a/src/pages/features/sections/GaiaCi.tsx +++ b/src/pages/features/sections/GaiaCi.tsx @@ -68,7 +68,7 @@ const TONE: Record = { const GaiaCi = () => (
diff --git a/src/pages/features/sections/LoadOnDemand.tsx b/src/pages/features/sections/LoadOnDemand.tsx index 9f114b3..3005d4c 100644 --- a/src/pages/features/sections/LoadOnDemand.tsx +++ b/src/pages/features/sections/LoadOnDemand.tsx @@ -133,7 +133,7 @@ const codeStyles = const LoadOnDemand = () => (
diff --git a/src/pages/features/sections/Principles.tsx b/src/pages/features/sections/Principles.tsx index fce5b4c..7e70ba0 100644 --- a/src/pages/features/sections/Principles.tsx +++ b/src/pages/features/sections/Principles.tsx @@ -9,7 +9,7 @@ const PRINCIPLES = [ const Principles = () => (
+ {'A '} + + bippy + + { + ' harness records every render to disk, then a deterministic CLI reduces tens of thousands of tokens of raw capture to the small ranked summary the model reads. The raw dump never enters context.' + } + + ), + name: 'The raw capture stays on disk', + }, + { + desc: 'It flags a memoized component that re-renders anyway when its props or context arrive as a new object or callback every render, and any component whose render crosses the frame budget, memo or not. A cheap re-render with real changes is left alone, so nothing pushes you toward memoizing everything.', + name: 'Two signals, not noise', + }, + { + desc: 'Findings lead with the memoized components that re-render anyway, then rank by wasted cost: how often each one re-renders times how long a render takes. A barely perceptible render-count win never outranks a memo that keeps re-rendering an expensive subtree on every parent update.', + name: 'Ranked by what users feel', + }, +]; + +const ReactPerf = () => ( +
+
+
+ +
+

+ A React app slows down one wasted render at a time. From the outside + the app just feels slow. From the inside, you are guessing which + component, and why. The profiler shows you every render and ranks + none of them. +

+

+ + /gaia-react-perf + + { + ' drives the interaction you point it at and captures real per-render attribution. The capture runs on the same React instrumentation that powers ' + } + + react-scan + + , wired into Claude, not a browser overlay. It surfaces two failure + modes: a memoized component that re-renders anyway because a prop or + context value it depends on is a new object or callback every + render, and any component whose render crosses a 16 ms frame budget, + memo or not. It skips the cheap re-renders that push you toward + memoizing everything, then names the structural fix for what is + left. +

+
+
+ + {/* Reduced summary · centerpiece */} +
+
+ /gaia-react-perf + + + Over budget + + 2 + +
+ +
+ {TOTALS.map((total) => ( + + {total.value} + {` ${total.label}`} + + ))} +
+ +
+

+ Ranked findings +

+ {FINDINGS.map((finding, index) => ( +
+ {index + 1} +
+
+ + {finding.component} + {finding.isMemo ? + + memo + + : null} + + + {`${finding.maxTotalMs} ms max`} + {finding.exceedsBudget ? + {` · over ${FRAME_BUDGET}`} + : null} + +
+

+ {finding.memoDefeated > 0 ? + `${finding.renderCount} renders · ${finding.memoDefeated} memo-defeated` + : `${finding.renderCount} renders`} +

+ {finding.inputs.length > 0 ? +

+ + {finding.source === 'context' ? + 'unstable context: ' + : 'unstable props: '} + + + {finding.inputs.join(', ')} + +

+ : null} + {finding.cause ? +

+ {'react-doctor · '} + {finding.cause} +

+ : null} +
+
+ ))} +
+ +
+

+ Diagnosis +

+

+ Two findings cross the 16 ms frame budget, and the fixes differ. + Give OrdersTable stable props so its memo holds, then cut the + per-render work in RevenueChart, which has no memo at all. +

+
+
+ +

+ Measure-only: GAIA diagnoses, Claude applies the fix you approve, then + re-runs /gaia-react-perf to confirm the finding drops to zero. +

+ + {/* Meta strip */} +
+ {META.map(({desc, name}) => ( +
+
+ {name} +
+
+ {desc} +
+
+ ))} +
+
+
+); + +export default ReactPerf; diff --git a/src/pages/features/sections/StateOnDisk.tsx b/src/pages/features/sections/StateOnDisk.tsx index 310d79a..5f37437 100644 --- a/src/pages/features/sections/StateOnDisk.tsx +++ b/src/pages/features/sections/StateOnDisk.tsx @@ -2,7 +2,7 @@ import TokenArtifacts from './figures/TokenArtifacts'; const StateOnDisk = () => (
diff --git a/src/pages/features/sections/UpdateDeps.tsx b/src/pages/features/sections/UpdateDeps.tsx index bf760ec..86563f9 100644 --- a/src/pages/features/sections/UpdateDeps.tsx +++ b/src/pages/features/sections/UpdateDeps.tsx @@ -120,7 +120,7 @@ const GaiaCard = () => ( const UpdateDeps = () => (
diff --git a/src/pages/features/sections/figures/TrustGraphic.tsx b/src/pages/features/sections/figures/TrustGraphic.tsx index d9345a0..8d665f8 100644 --- a/src/pages/features/sections/figures/TrustGraphic.tsx +++ b/src/pages/features/sections/figures/TrustGraphic.tsx @@ -5,7 +5,7 @@ type Stage = { const STAGES: Stage[] = [ {label: 'typecheck', result: '0 errors'}, - {label: 'lint · 1,314 rules', result: '0 violations'}, + {label: 'lint · 1,450 rules', result: '0 violations'}, {label: 'tests · vitest + RTL', result: '47 / 47'}, {label: 'build · 9 entries', result: 'green'}, ]; diff --git a/src/pages/get-started/sections/GetStarted.tsx b/src/pages/get-started/sections/GetStarted.tsx index 052a2c8..d26f431 100644 --- a/src/pages/get-started/sections/GetStarted.tsx +++ b/src/pages/get-started/sections/GetStarted.tsx @@ -786,7 +786,7 @@ const RECAP = [ title: 'Project memory & rules', }, { - body: '1,314 lint rules on every commit. A code-review-audit agent on every merge: security, performance, architecture.', + body: '1,450 lint rules on every commit. A code-review-audit agent on every merge: security, performance, architecture.', title: 'Quality gates', }, { @@ -812,7 +812,7 @@ const RECAP = [ const WhatYouGet = () => (
diff --git a/src/pages/why/sections/Diagnosis.tsx b/src/pages/why/sections/Diagnosis.tsx index dae87aa..7eb5c16 100644 --- a/src/pages/why/sections/Diagnosis.tsx +++ b/src/pages/why/sections/Diagnosis.tsx @@ -38,7 +38,7 @@ const FAILURES: Item[] = [ const Diagnosis = () => (
diff --git a/src/pages/why/sections/Discipline.tsx b/src/pages/why/sections/Discipline.tsx index c425197..f37e76d 100644 --- a/src/pages/why/sections/Discipline.tsx +++ b/src/pages/why/sections/Discipline.tsx @@ -1,6 +1,6 @@ const Discipline = () => (