From ab705279daf81a4a532e403e8b711473fa050db9 Mon Sep 17 00:00:00 2001 From: oxwen11 Date: Mon, 2 Feb 2026 22:58:25 +0800 Subject: [PATCH 1/3] docs: add react-scan integration design Document the approach for integrating react-scan into the desktop renderer for performance visualization and debugging during development. --- ...026-02-02-react-scan-integration-design.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 docs/plans/2026-02-02-react-scan-integration-design.md diff --git a/docs/plans/2026-02-02-react-scan-integration-design.md b/docs/plans/2026-02-02-react-scan-integration-design.md new file mode 100644 index 0000000..186b9c5 --- /dev/null +++ b/docs/plans/2026-02-02-react-scan-integration-design.md @@ -0,0 +1,122 @@ +# React-scan Integration Design + +**Date:** 2026-02-02 +**Target:** Desktop Renderer +**Goal:** Integrate react-scan for performance visualization and debugging + +## Overview + +Integrate react-scan into the desktop renderer to provide real-time performance monitoring during development. React-scan will highlight component re-renders, detect unnecessary renders, and provide console diagnostics to help identify performance bottlenecks. + +## Configuration + +**Dependency:** +- Add `react-scan` as dev dependency to `apps/desktop` +- Installation: `pnpm add react-scan -D --filter desktop` + +**Settings:** +- `enabled`: Controlled by `import.meta.env.DEV` (development-only) +- `showToolbar`: `true` (draggable UI for runtime control) +- `animationSpeed`: `"fast"` (visible but not distracting) +- `log`: `true` (console output for render tracking) +- `trackUnnecessaryRenders`: `true` (detect renders with no DOM changes) + +## Implementation + +### Critical Requirement + +React-scan must hijack React DevTools hooks before React initializes. This requires importing and configuring react-scan BEFORE any React imports in the entry point. + +### File Changes + +**`apps/desktop/src/renderer/src/main.tsx`:** + +```typescript +// CRITICAL: react-scan must be imported FIRST, before React +import { scan } from 'react-scan' + +// Configure react-scan before any React imports +if (import.meta.env.DEV) { + scan({ + enabled: true, + showToolbar: true, + animationSpeed: 'fast', + log: true, + trackUnnecessaryRenders: true, + }) +} + +// NOW import React and other dependencies +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +// ... rest of imports and app initialization +``` + +### Import Order + +1. `react-scan` import and configuration +2. React imports (`react`, `react-dom`) +3. Application imports +4. Render logic + +Breaking this order will cause react-scan to fail silently. + +## Verification + +### Visual Indicators + +When running `pnpm dev --filter desktop`, the following should be visible: + +- **Toolbar:** Draggable UI element (typically bottom-right) +- **Render highlights:** Colored outlines flash on component re-renders +- **Color coding:** Cooler colors (blue) = infrequent, warmer colors (red) = frequent + +### Console Output + +With `log: true`, expect console messages: + +``` +[react-scan] Component rendered: +[react-scan] Unnecessary render detected in +``` + +### Toolbar Features + +- Toggle scanning on/off +- View render statistics per component +- Inspect individual render causes +- Performance bell icon for insights +- Drag to screen edges to collapse + +### Expected Behavior + +Areas likely to show render activity: +- **Terminal components:** Active during typing/output +- **Sidebar navigation:** Highlights on route changes +- **Dialogs:** Render on open/close +- **Task/worktree lists:** Update on state changes + +## Development Workflow + +1. Start dev server: `pnpm dev --filter desktop` +2. Open Electron app +3. Interact with UI to trigger renders +4. Observe toolbar and console for performance data +5. Use insights to identify optimization opportunities + +## Success Criteria + +- [ ] React-scan toolbar visible in dev mode +- [ ] Component renders highlighted with colored outlines +- [ ] Console logs show render activity +- [ ] Unnecessary renders detected and reported +- [ ] No performance impact in production builds +- [ ] Type safety maintained (TypeScript errors = 0) + +## Notes + +- React-scan only runs in development mode (Vite's `import.meta.env.DEV`) +- Production builds remain unaffected (no bundle size impact) +- Toolbar can be hidden by dragging to screen edges +- Fast animation speed balances visibility with minimal distraction From 88bbbe982e04697f2133ba0e7e66f9a65d91642a Mon Sep 17 00:00:00 2001 From: oxwen11 Date: Mon, 2 Feb 2026 23:04:15 +0800 Subject: [PATCH 2/3] feat(desktop): integrate react-scan for performance monitoring Add react-scan to desktop renderer for real-time component render visualization and performance debugging during development. Changes: - Add react-scan as dev dependency - Configure react-scan in main.tsx before React imports - Enable full diagnostics: toolbar, logging, unnecessary render tracking - Update design document with implementation status React-scan only runs in development mode and has no production impact. --- apps/desktop/package.json | 1 + apps/desktop/src/renderer/src/main.tsx | 15 ++ ...026-02-02-react-scan-integration-design.md | 12 +- pnpm-lock.yaml | 143 ++++++++++++++++++ 4 files changed, 165 insertions(+), 6 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7a719d5..5b690e8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -65,6 +65,7 @@ "p-queue": "^9.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-scan": "^0.4.3", "tailwind-merge": "^3.3.0", "tailwindcss": "^4.1.12", "typescript": "^5.9.2", diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index 8fb0ece..10f88e4 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -1,3 +1,18 @@ +// CRITICAL: react-scan must be imported and configured BEFORE React +import { scan } from "react-scan"; + +// Configure react-scan before any React imports +if (import.meta.env.DEV) { + scan({ + enabled: true, + showToolbar: true, + animationSpeed: "fast", + log: true, + trackUnnecessaryRenders: true, + }); +} + +// NOW import React and other dependencies import { QueryClientProvider } from "@tanstack/react-query"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; diff --git a/docs/plans/2026-02-02-react-scan-integration-design.md b/docs/plans/2026-02-02-react-scan-integration-design.md index 186b9c5..fa66c2d 100644 --- a/docs/plans/2026-02-02-react-scan-integration-design.md +++ b/docs/plans/2026-02-02-react-scan-integration-design.md @@ -107,12 +107,12 @@ Areas likely to show render activity: ## Success Criteria -- [ ] React-scan toolbar visible in dev mode -- [ ] Component renders highlighted with colored outlines -- [ ] Console logs show render activity -- [ ] Unnecessary renders detected and reported -- [ ] No performance impact in production builds -- [ ] Type safety maintained (TypeScript errors = 0) +- [ ] React-scan toolbar visible in dev mode (requires running app to verify) +- [ ] Component renders highlighted with colored outlines (requires running app to verify) +- [ ] Console logs show render activity (requires running app to verify) +- [ ] Unnecessary renders detected and reported (requires running app to verify) +- [x] No performance impact in production builds (react-scan only enabled in dev mode) +- [x] Type safety maintained (TypeScript errors = 0) ## Notes diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53ff3a9..b822417 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,6 +226,9 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.4(react@19.2.4) + react-scan: + specifier: ^0.4.3 + version: 0.4.3(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1) tailwind-merge: specifier: ^3.3.0 version: 3.4.0 @@ -1605,6 +1608,12 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@clerk/clerk-react@5.60.0': resolution: {integrity: sha512-P88FncsJpq/3WZJhhlj+md8mYb35BIXpr462C/figwsBGHsinr8VuBQUMcMZZ/6M34C8ABfLTPa6PHVp6+3D5Q==} engines: {node: '>=18.17.0'} @@ -2605,6 +2614,12 @@ packages: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 + '@pivanov/utils@0.0.2': + resolution: {integrity: sha512-q9CN0bFWxWgMY5hVVYyBgez1jGiLBa6I+LkG37ycylPhFvEGOOeaADGtUSu46CaZasPnlY8fCdVJZmrgKb1EPA==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2617,6 +2632,14 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@preact/signals-core@1.12.2': + resolution: {integrity: sha512-5Yf8h1Ke3SMHr15xl630KtwPTW4sYDFkkxS0vQ8UiQLWwZQnrF9IKaVG1mN5VcJz52EcWs2acsc/Npjha/7ysA==} + + '@preact/signals@1.3.3': + resolution: {integrity: sha512-FieIS2Z3iHAmjYTqsD3U3DEad1/bOicGN8wT34bbrdp422kQfkP7iwYgqTgx53wPdnsENaNkNNmScbi3RVZq8Q==} + peerDependencies: + preact: 10.x + '@publint/pack@0.1.3': resolution: {integrity: sha512-dHDWeutAerz+Z2wFYAce7Y51vd4rbLBfUh0BNnyul4xKoVsPUVJBrOAFsJvtvYBwGFJSqKsxyyHf/7evZ8+Q5Q==} engines: {node: '>=18'} @@ -4243,6 +4266,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + '@types/node@22.19.7': resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} @@ -4260,6 +4286,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -5209,6 +5240,11 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bippy@0.3.34: + resolution: {integrity: sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ==} + peerDependencies: + react: '>=17.0.1' + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} @@ -6715,6 +6751,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + knip@5.82.1: resolution: {integrity: sha512-1nQk+5AcnkqL40kGQXfouzAEXkTR+eSrgo/8m1d0BMei4eAzFwghoXC4gOKbACgBiCof7hE8wkBVDsEvznf85w==} engines: {node: '>=18.18.0'} @@ -7561,6 +7601,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + preact@10.28.3: + resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7798,6 +7841,26 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + react-scan@0.4.3: + resolution: {integrity: sha512-jhAQuQ1nja6HUYrSpbmNFHqZPsRCXk8Yqu0lHoRIw9eb8N96uTfXCpVyQhTTnJ/nWqnwuvxbpKVG/oWZT8+iTQ==} + hasBin: true + peerDependencies: + '@remix-run/react': '>=1.0.0' + next: '>=13.0.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-router: ^5.0.0 || ^6.0.0 || ^7.0.0 + react-router-dom: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -8693,6 +8756,10 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -9691,6 +9758,17 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clerk/clerk-react@5.60.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@clerk/shared': 3.44.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10607,6 +10685,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) shiki: 3.12.2 + '@pivanov/utils@0.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@pkgjs/parseargs@0.11.0': optional: true @@ -10616,6 +10699,13 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@preact/signals-core@1.12.2': {} + + '@preact/signals@1.3.3(preact@10.28.3)': + dependencies: + '@preact/signals-core': 1.12.2 + preact: 10.28.3 + '@publint/pack@0.1.3': {} '@quansync/fs@1.0.0': @@ -12200,6 +12290,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + '@types/node@22.19.7': dependencies: undici-types: 6.21.0 @@ -12218,6 +12312,10 @@ snapshots: dependencies: '@types/react': 19.2.10 + '@types/react-reconciler@0.28.9(@types/react@19.2.10)': + dependencies: + '@types/react': 19.2.10 + '@types/react-syntax-highlighter@15.5.13': dependencies: '@types/react': 19.2.10 @@ -13951,6 +14049,13 @@ snapshots: binary-extensions@2.3.0: {} + bippy@0.3.34(@types/react@19.2.10)(react@19.2.4): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.10) + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + birpc@2.9.0: {} bl@4.1.0: @@ -15577,6 +15682,8 @@ snapshots: kleur@3.0.3: {} + kleur@4.1.5: {} + knip@5.82.1(@types/node@22.19.7)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -16490,6 +16597,8 @@ snapshots: commander: 9.5.0 optional: true + preact@10.28.3: {} + prelude-ls@1.2.1: {} prettier-plugin-tailwindcss@0.6.14(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.27)(prettier@3.8.1))(prettier@3.8.1): @@ -16712,6 +16821,34 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + react-scan@0.4.3(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1): + dependencies: + '@babel/core': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/types': 7.28.6 + '@clack/core': 0.3.5 + '@clack/prompts': 0.8.2 + '@pivanov/utils': 0.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@preact/signals': 1.3.3(preact@10.28.3) + '@rollup/pluginutils': 5.3.0(rollup@4.57.1) + '@types/node': 20.19.30 + bippy: 0.3.34(@types/react@19.2.10)(react@19.2.4) + esbuild: 0.25.12 + estree-walker: 3.0.3 + kleur: 4.1.5 + mri: 1.2.0 + playwright: 1.58.1 + preact: 10.28.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tsx: 4.21.0 + optionalDependencies: + unplugin: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - rollup + - supports-color + react-style-singleton@2.2.3(@types/react@19.2.10)(react@19.2.4): dependencies: get-nonce: 1.0.1 @@ -17669,6 +17806,12 @@ snapshots: acorn: 8.15.0 webpack-virtual-modules: 0.6.2 + unplugin@2.1.0: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + optional: true + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 From 661705ba1d7a27d803f54cb12c38840b24fc397b Mon Sep 17 00:00:00 2001 From: oxwen11 Date: Thu, 5 Feb 2026 00:26:06 +0800 Subject: [PATCH 3/3] fix(desktop): update CSP to allow react-scan blob workers React-scan requires blob: workers for its performance monitoring overlay. Updated CSP to include blob: in script-src and worker-src directives. Also adjusted react-scan config for better debugging. --- apps/desktop/src/renderer/index.html | 2 +- apps/desktop/src/renderer/src/main.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index a4b3472..47d5286 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -6,7 +6,7 @@ diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index 10f88e4..40d43fa 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -6,9 +6,12 @@ if (import.meta.env.DEV) { scan({ enabled: true, showToolbar: true, - animationSpeed: "fast", - log: true, + animationSpeed: "slow", + log: false, trackUnnecessaryRenders: true, + onRender: (fiber, renders) => { + console.log("[react-scan] render:", fiber.type?.name || fiber.type, renders.length); + }, }); }