diff --git a/knip.json b/knip.json index 0901fcb18..d0702b9de 100644 --- a/knip.json +++ b/knip.json @@ -7,7 +7,8 @@ "scripts/react/profiler.ts!", "scripts/react/boundaries.tsx!", "scripts/react/material.tsx!", - "scripts/react/yaml.ts!" + "scripts/react/yaml.ts!", + "scripts/react/wdyr.ts!" ], "project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"], "include": [ diff --git a/package.json b/package.json index 6efbdaa30..f79fb5dc8 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "perf:compare": "node scripts/perf/compare.mjs", "perf:test": "node --test scripts/perf/report.test.mjs", "dev:profile": "cross-env CVR_REACT_DIAGNOSTICS=profiler node scripts/dev.mjs dev", - "react:ab": "node scripts/react/ab.mjs" + "react:ab": "node scripts/react/ab.mjs", + "dev:wdyr": "cross-env CVR_REACT_DIAGNOSTICS=wdyr node scripts/dev.mjs dev" }, "dependencies": { "@dnd-kit/collision": "^0.5.0", @@ -137,7 +138,8 @@ "vitest": "^5.0.1", "webdriverio": "10.1.0", "ws": "8.22.1", - "playwright": "1.64.1" + "playwright": "1.64.1", + "@welldone-software/why-did-you-render": "10.0.1" }, "lint-staged": { "*.{ts,tsx,js,mjs}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96a9d3e73..a15dcf9aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -341,6 +341,9 @@ importers: '@wdio/tauri-service': specifier: 1.5.0 version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) + '@welldone-software/why-did-you-render': + specifier: 10.0.1 + version: 10.0.1(react@19.3.1) adm-zip: specifier: ^0.6.1 version: 0.6.1 @@ -2681,6 +2684,11 @@ packages: resolution: {integrity: sha512-BABJY+L4JidOD2bfQe3icuRZ0jQeUimTwUEe66I4BHVGWwaa5QBeVU+JZmdNQLjMA9q+Nk385aL96Mb1/PbKYw==} engines: {node: '>=22.19.0'} + '@welldone-software/why-did-you-render@10.0.1': + resolution: {integrity: sha512-tMgGkt30iVYeLMUKExNmtm019QgyjLtA7lwB0QAizYNEuihlCG2eoAWBBaz/bDeI7LeqAJ9msC6hY3vX+JB97g==} + peerDependencies: + react: ^19 + '@zip.js/zip.js@2.24.0': resolution: {integrity: sha512-WlaVKzdqBK+RF3bdiXZWbdCnvGB6PdO4IHC1mn/2Yfl3bASgqUNegnU3+PNg61iecwoPfUi5BgLUPqBukXpyWw==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} @@ -7389,6 +7397,11 @@ snapshots: - supports-color - yauzl + '@welldone-software/why-did-you-render@10.0.1(react@19.3.1)': + dependencies: + lodash: 4.18.1 + react: 19.3.1 + '@zip.js/zip.js@2.24.0': {} abort-controller@3.0.0: diff --git a/scripts/react/README.md b/scripts/react/README.md index 10d4ff11e..fe07fcc62 100644 --- a/scripts/react/README.md +++ b/scripts/react/README.md @@ -6,6 +6,13 @@ copies of the latest 1,000 standard Profiler callback records; `.clear()` drops them. Records contain timing and commit metadata, never business props. Normal development and production builds do not load the collector. +`pnpm dev:wdyr` instead initializes why-did-you-render before loading the app +and uses its development JSX runtime. It reports avoidable renders for +`ProxyItem`, `ProxyItemMini` and `ProxyTunCard` in the console. The modes are +mutually exclusive; `CVR_REACT_DIAGNOSTICS` accepts only `profiler` or `wdyr` on +the development server. Each mode uses its own Vite cache. Production builds +ignore this variable and include neither diagnostic entry nor runtime. + Install the browser explicitly with `pnpm exec playwright install chromium`. Then run `pnpm react:ab --base --head `. Both Git trees run against one installed dependency set and the same fixed DNS, delay-hook and proxy-tab diff --git a/scripts/react/vite.mts b/scripts/react/vite.mts index 753a7f716..4a5989866 100644 --- a/scripts/react/vite.mts +++ b/scripts/react/vite.mts @@ -10,10 +10,39 @@ export function reactDiagnostics(mode: string | undefined): Plugin[] { enforce: 'pre', apply: 'serve', config() { - if (mode !== 'profiler') + if (mode !== 'profiler' && mode !== 'wdyr') throw new Error(`Unknown CVR_REACT_DIAGNOSTICS mode: ${mode}`) + return { + cacheDir: path.resolve('node_modules/.vite', mode), + ...(mode === 'wdyr' + ? { + oxc: { + jsx: { + importSource: '@welldone-software/why-did-you-render', + }, + }, + optimizeDeps: { + include: [ + '@welldone-software/why-did-you-render', + '@welldone-software/why-did-you-render/jsx-dev-runtime', + ], + }, + } + : {}), + } + }, + transformIndexHtml(html) { + if (mode !== 'wdyr') return html + const entry = 'src="./main.tsx"' + if (!html.includes(entry)) + throw new Error('WDYR entry no longer matches index.html') + return html.replace( + entry, + `src="/@fs/${normalizePath(path.resolve('scripts/react/wdyr.ts'))}"`, + ) }, transform(code, id) { + if (mode !== 'profiler') return if (id !== normalizePath(path.resolve('src/main.tsx'))) return const open = '' const close = '' diff --git a/scripts/react/wdyr.ts b/scripts/react/wdyr.ts new file mode 100644 index 000000000..ec8d409e9 --- /dev/null +++ b/scripts/react/wdyr.ts @@ -0,0 +1,10 @@ +import whyDidYouRender from '@welldone-software/why-did-you-render' +import React from 'react' + +whyDidYouRender(React, { + include: [/^(ProxyItem|ProxyItemMini|ProxyTunCard)$/], + trackAllPureComponents: false, + trackHooks: true, +}) + +await import('../../src/main') diff --git a/vite.config.mts b/vite.config.mts index f60f5e6f0..a8a5fa3f9 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -9,6 +9,7 @@ import { reactDiagnostics } from './scripts/react/vite.mts' export default defineConfig({ root: 'src', + cacheDir: path.resolve('node_modules/.vite/default'), server: { port: 3000 }, plugins: [ svgr(),