Repository: aidenybai/react-scan Branch: main Commit: b5be21653951 Files: 238 Total size: 1.5 MB Directory structure: gitextract_xag52wf6/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ ├── CODE_OF_CONDUCT.md │ └── workflows/ │ ├── build-extension.yml │ └── pkg-pr-new.yaml ├── .gitignore ├── .npmrc ├── .oxlintrc.json ├── .vscode/ │ └── settings.json ├── AGENTS.md ├── BROWSER_EXTENSION_GUIDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin/ │ ├── generate-certs.sh │ └── serve-scan.sh ├── docs/ │ └── installation/ │ ├── astro.md │ ├── cdn.md │ ├── create-react-app.md │ ├── next-js-app-router.md │ ├── next-js-page-router.md │ ├── parcel.md │ ├── react-router.md │ ├── remix.md │ ├── rsbuild.md │ ├── tanstack-start.md │ └── vite.md ├── e2e/ │ ├── helpers.ts │ ├── inspector.spec.ts │ ├── notifications.spec.ts │ ├── outlines.spec.ts │ └── toolbar.spec.ts ├── kitchen-sink/ │ ├── index.html │ ├── package.json │ ├── postcss.config.mjs │ ├── src/ │ │ ├── examples/ │ │ │ ├── e2e-fixture/ │ │ │ │ └── index.tsx │ │ │ ├── sierpinski/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ └── todo-list/ │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── index.css │ │ ├── index.tsx │ │ ├── main.css │ │ └── main.tsx │ ├── tailwind.config.mjs │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages/ │ ├── extension/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── assets/ │ │ │ │ └── css/ │ │ │ │ └── no-react.css │ │ │ ├── background/ │ │ │ │ ├── icon.ts │ │ │ │ └── index.ts │ │ │ ├── content/ │ │ │ │ └── index.ts │ │ │ ├── inject/ │ │ │ │ ├── index.ts │ │ │ │ ├── notification.ts │ │ │ │ └── react-scan.ts │ │ │ ├── manifest.chrome.json │ │ │ ├── manifest.firefox.json │ │ │ ├── types/ │ │ │ │ ├── global.d.ts │ │ │ │ └── messages.ts │ │ │ ├── utils/ │ │ │ │ ├── constants.ts │ │ │ │ └── helpers.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── scan/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── auto.d.ts │ │ ├── bin/ │ │ │ └── cli.js │ │ ├── global.d.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── postcss.rem2px.mjs │ │ ├── scripts/ │ │ │ └── bump-version.js │ │ ├── src/ │ │ │ ├── auto.ts │ │ │ ├── cli-utils.mts │ │ │ ├── cli-utils.test.mts │ │ │ ├── cli.mts │ │ │ ├── core/ │ │ │ │ ├── all-environments.ts │ │ │ │ ├── fast-serialize.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ ├── notifications/ │ │ │ │ │ ├── event-tracking.ts │ │ │ │ │ ├── interaction-store.ts │ │ │ │ │ ├── outline-overlay.ts │ │ │ │ │ ├── performance-store.ts │ │ │ │ │ ├── performance-utils.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── types.ts │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ ├── install-hook.ts │ │ │ ├── monitoring/ │ │ │ │ └── next.ts │ │ │ ├── new-outlines/ │ │ │ │ ├── canvas.ts │ │ │ │ ├── index.ts │ │ │ │ ├── offscreen-canvas.worker.ts │ │ │ │ └── types.ts │ │ │ ├── polyfills.ts │ │ │ ├── react-component-name/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── arrow-function.test.ts │ │ │ │ │ ├── complex-patterns.test.ts │ │ │ │ │ ├── function-declarations.test.ts │ │ │ │ │ ├── general-cases.test.ts │ │ │ │ │ ├── react-patterns.test.ts │ │ │ │ │ ├── ts-patterns.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── astro.ts │ │ │ │ ├── babel/ │ │ │ │ │ ├── get-descriptive-name.ts │ │ │ │ │ ├── get-root-statement-path.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── is-componentish-name.ts │ │ │ │ │ ├── is-nested-expression.ts │ │ │ │ │ ├── is-path-valid.ts │ │ │ │ │ ├── is-statement-top-level.ts │ │ │ │ │ ├── path-references-import.ts │ │ │ │ │ └── unwrap.ts │ │ │ │ ├── core/ │ │ │ │ │ └── options.ts │ │ │ │ ├── esbuild.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── rolldown.ts │ │ │ │ ├── rollup.ts │ │ │ │ ├── rspack.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vite.ts │ │ │ │ └── webpack.ts │ │ │ ├── types.d.ts │ │ │ ├── types.ts │ │ │ ├── web/ │ │ │ │ ├── assets/ │ │ │ │ │ └── css/ │ │ │ │ │ └── styles.tailwind.css │ │ │ │ ├── components/ │ │ │ │ │ ├── copy-to-clipboard/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── icon/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── slider/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── sticky-section/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── svg-sprite/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── toggle/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-delayed-value.ts │ │ │ │ │ ├── use-merged-refs.ts │ │ │ │ │ └── use-virtual-list.ts │ │ │ │ ├── state.ts │ │ │ │ ├── toolbar.tsx │ │ │ │ ├── utils/ │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── create-store.ts │ │ │ │ │ ├── geiger.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── log.ts │ │ │ │ │ ├── pin.ts │ │ │ │ │ └── preact/ │ │ │ │ │ └── constant.ts │ │ │ │ ├── views/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── inspector/ │ │ │ │ │ │ ├── components-tree/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── state.ts │ │ │ │ │ │ ├── diff-value.tsx │ │ │ │ │ │ ├── flash-overlay.ts │ │ │ │ │ │ ├── header.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── logging.ts │ │ │ │ │ │ ├── overlay/ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── properties.tsx │ │ │ │ │ │ ├── states.ts │ │ │ │ │ │ ├── timeline/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ ├── utils.ts │ │ │ │ │ │ ├── what-changed.tsx │ │ │ │ │ │ └── whats-changed/ │ │ │ │ │ │ └── use-change-store.ts │ │ │ │ │ ├── notifications/ │ │ │ │ │ │ ├── collapsed-event.tsx │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ ├── details-routes.tsx │ │ │ │ │ │ ├── icons.tsx │ │ │ │ │ │ ├── notification-header.tsx │ │ │ │ │ │ ├── notification-tabs.tsx │ │ │ │ │ │ ├── notifications.tsx │ │ │ │ │ │ ├── optimize.tsx │ │ │ │ │ │ ├── other-visualization.tsx │ │ │ │ │ │ ├── popover.tsx │ │ │ │ │ │ ├── render-bar-chart.tsx │ │ │ │ │ │ ├── render-explanation.tsx │ │ │ │ │ │ └── slowdown-history.tsx │ │ │ │ │ ├── settings/ │ │ │ │ │ │ └── header.tsx │ │ │ │ │ └── toolbar/ │ │ │ │ │ └── index.tsx │ │ │ │ └── widget/ │ │ │ │ ├── fps-meter.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── helpers.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── resize-handle.tsx │ │ │ │ └── types.ts │ │ │ └── worker-shim.ts │ │ ├── tailwind.config.mjs │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ ├── vite.config.mts │ │ └── worker-plugin.ts │ ├── vite-plugin-react-scan/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── global.d.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── website/ │ ├── .gitignore │ ├── .oxlintrc.json │ ├── AGENTS.md │ ├── README.md │ ├── app/ │ │ ├── api/ │ │ │ └── waitlist/ │ │ │ └── route.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── react-scan.ts │ │ └── replay/ │ │ └── page.tsx │ ├── components/ │ │ ├── cli.tsx │ │ ├── code.tsx │ │ ├── companies.tsx │ │ ├── counter.tsx │ │ ├── footer.tsx │ │ ├── header.tsx │ │ ├── icons/ │ │ │ ├── icon-discord.tsx │ │ │ ├── icon-github.tsx │ │ │ └── types.ts │ │ ├── install-guide.tsx │ │ ├── test-data-types.tsx │ │ └── todo-demo.tsx │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public/ │ │ └── auto.global.js │ ├── tailwind.config.ts │ └── tsconfig.json ├── playwright.config.ts ├── pnpm-workspace.yaml ├── scripts/ │ ├── build-worker.ts │ ├── bump-version.js │ ├── version-warning.mjs │ └── workspace.mjs └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [["react-scan", "@react-scan/extension"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@react-scan/website"] } ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aiden.bai05@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: .github/workflows/build-extension.yml ================================================ name: Build Extension on: push: branches: - main paths: - 'packages/extension/**' jobs: build: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: main - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup PNPM uses: pnpm/action-setup@v2 with: version: 9.x - name: Install dependencies run: pnpm install - name: Get package info id: package run: | echo "name=$(node -p "require('./packages/extension/package.json').name")" >> $GITHUB_OUTPUT echo "version=$(node -p "require('./packages/extension/package.json').version")" >> $GITHUB_OUTPUT - name: Build extensions run: | pnpm build cd packages/extension rm -rf build pnpm pack:all - name: Commit changes if: github.ref == 'refs/heads/main' run: | git checkout main git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add -f packages/extension/build/*.zip git diff --staged --quiet || (git commit -m "chore: update extension builds [skip ci]" && git push) ================================================ FILE: .github/workflows/pkg-pr-new.yaml ================================================ name: Publish Any Commit on: push: branches: - "**" pull_request: branches: - "**" jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18] steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 9.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile --strict-peer-dependencies=false - name: Build run: | cd packages/scan NODE_ENV=production pnpm build env: NODE_ENV: production - name: Publish NPM Package to pkg-pr-new run: pnpx pkg-pr-new publish ./packages/scan env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ node_modules .DS_Store .env dist **/*.tgz *.log build !packages/extension/build/ playgrounds # SSL Certificates bin/certs/*.pem bin/certs/*.key .cursor # Playwright test-results/ playwright-report/ ================================================ FILE: .npmrc ================================================ prefer-workspace-packages=true link-workspace-packages=true ================================================ FILE: .oxlintrc.json ================================================ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "react", "import"], "ignorePatterns": [ "dist", "build", "node_modules", "**/*.css", "**/*.astro" ], "categories": {}, "rules": { "no-unused-vars": [ "warn", { "vars": "all", "args": "all", "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrors": "none" } ], "no-unused-labels": "warn", "no-unused-private-class-members": "warn", "no-console": "warn", "typescript/no-explicit-any": "warn", "typescript/no-non-null-assertion": "warn", "react/no-danger": "error", "react-hooks/exhaustive-deps": "warn" } } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "css.lint.unknownAtRules": "ignore", "oxc.lint.enable": true, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: AGENTS.md ================================================ ## General Rules - MUST: Use TypeScript interfaces over types. - MUST: Keep all types in the global scope. - MUST: Use arrow functions over function declarations - MUST: Never comment unless absolutely necessary. - If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack - MUST: Use kebab-case for files - MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names). - Example: for .map(), you can use `innerX` instead of `x` - Example: instead of `moved` use `didPositionChange` - MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive. - MUST: Do not type cast ("as") unless absolutely necessary - MUST: Remove unused code and don't repeat yourself. - MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution. - MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`). - MUST: Put small, focused utility functions in `utils/` with one utility per file. - MUST: Use Boolean over !!. ## Testing Run checks always before committing with: ```bash pnpm build pnpm lint pnpm format ``` ================================================ FILE: BROWSER_EXTENSION_GUIDE.md ================================================ # Browser Extension Installation Guide > [!NOTE] > The React Scan browser extension currently uses `react-scan@0.4.3` ## Chrome > You can download the Chrome browser extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/react-scan/anmmhkomejbdklkhoiloeaehppaffmdf). Below is a guide to installing the extension manually. 1. Download the [`chrome-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Chrome and navigate to `chrome://extensions/`. 4. Enable "Developer mode" if it is not already enabled. 5. Click "Load unpacked" and select the unzipped folder (or drag the folder into the page). ## Firefox > React Scan's Browser extension is still pending approvals from Firefox Add-ons. Below is a guide to installing the extension manually. 1. Download the [`firefox-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`. 4. Click "Load Temporary Add-on..." 5. Select `manifest.json` from the unzipped folder ## Brave > React Scan's Browser extension is still pending approvals from Brave Browser. Below is a guide to installing the extension manually. 1. Download the [`brave-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Brave and navigate to `brave://extensions`. 4. Click "Load unpacked" and select the unzipped folder (or drag the folder into the page). ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to React Scan First off, thanks for taking the time to contribute! ❤️ ## Table of Contents - [Contributing to React Scan](#contributing-to-react-scan) - [Table of Contents](#table-of-contents) - [Project Structure](#project-structure) - [Development Setup](#development-setup) - [Contributing Guidelines](#contributing-guidelines) - [Commits](#commits) - [Pull Request Process](#pull-request-process) - [Development Workflow](#development-workflow) - [Getting Help](#getting-help) ## Project Structure This is a monorepo containing several packages: - `packages/scan` - Core React Scan package - `packages/vite-plugin-react-scan` - Vite plugin for React Scan - `packages/extension` - VS Code extension ## Development Setup 1. **Clone and Install** ```bash git clone https://github.com/aidenybai/react-scan.git cd react-scan pnpm install ``` 2. **Build all packages** ```bash pnpm build ``` 3. **Testing React Scan** ```bash cd packages/scan pnpm build:copy ``` - This will build the package and then copy it to your clipboard as an IIFE (immedietely invoked function expression). This will allow you to paste it into the browser console to test it on any website https://github.com/user-attachments/assets/f279e664-479f-4e39-bff4-1bbfee30af22 ## Contributing Guidelines ### Commits We use conventional commits to ensure consistent commit messages: - `feat:` New features - `fix:` Bug fixes - `docs:` Documentation changes - `chore:` Maintenance tasks - `test:` Adding or updating tests - `refactor:` Code changes that neither fix bugs nor add features Example: `fix(scan): fix a typo` ### Pull Request Process 1. Fork the repository 2. Create your feature branch (`git checkout -b feat/amazing-feature`) 3. Commit your changes using conventional commits 4. Push to your branch 5. Open a Pull Request 6. Ask for reviews (@pivanov, @RobPruzan are your friends in this journey) ### Development Workflow 1. **TypeScript** - All code must be written in TypeScript - Ensure strict type checking passes - No `any` types unless absolutely necessary 2. **Code Style** - We use Biome for formatting and linting - Run `pnpm format` to format code - Run `pnpm lint` to check for issues 3. **Documentation** - Update relevant documentation - Add JSDoc comments for public APIs - Update README if needed ## Getting Help - Check existing issues - Create a new issue
⚛️ Happy coding! 🚀 ================================================ FILE: LICENSE ================================================ Copyright 2025 Aiden Bai, Million Software, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # React Scan React Scan automatically detects performance issues in your React app. - Requires no code changes -- just drop it in - Highlights exactly the components you need to optimize - Always accessible through a toolbar on page ### Quick Start ```bash npx -y react-scan@latest init ``` ### [**Try out a demo! →**](https://react-scan.million.dev) React Scan in action ## Install The `init` command will automatically detect your framework, install `react-scan` via npm, and set up your project. ```bash npx -y react-scan@latest init ``` ### Manual Installation Install the package: ```bash npm install -D react-scan ``` Then add the script tag to your app. Pick the guide for your framework: #### Script Tag Paste this before any scripts in your `index.html`: ```html ``` #### Next.js (App Router) Add this inside of your `app/layout.tsx`: ```tsx import Script from "next/script"; export default function RootLayout({ children }) { return (
``` #### Remix Add this inside your `app/root.tsx`: ```tsx import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; export default function App() { return ( ``` If you want react-scan to also run in production, use the react-scan/all-environments import path ```diff - import { scan } from "react-scan"; + import { scan } from "react-scan/all-environments"; ``` ================================================ FILE: docs/installation/cdn.md ================================================ # CDN You can choose one of the following URLs to initialize React Scan via CDN. ## Usage ```html ``` ## Available URLs ### JSDelivr ```txt https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js ``` ### UNPKG ```txt https://unpkg.com/react-scan/dist/auto.global.js ``` ================================================ FILE: docs/installation/create-react-app.md ================================================ # Create React App (CRA) Guide ## As a script tag Add the script tag to your `index.html`. Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs. ```html ``` ## As a module import In your project entrypoint (e.g. `src/index`, `src/main`): ```jsx // src/index.jsx // must be imported before React and React DOM import { scan } from "react-scan"; import React from "react"; scan({ enabled: true, }); ``` If you want react-scan to also run in production, use the react-scan/all-environments import path ```diff - import { scan } from "react-scan"; + import { scan } from "react-scan/all-environments"; ``` > [!CAUTION] > React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it. ================================================ FILE: docs/installation/next-js-app-router.md ================================================ # NextJS App Router Guide ## As a script tag Add the script tag to your `app/layout`. Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs. ```jsx // app/layout export default function RootLayout({ children }) { return ( ``` ================================================ FILE: docs/installation/react-router.md ================================================ # React Router v7 Guide ## As a script tag Add the script tag to your `Layout` component in the `app/root`. Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs. ```jsx // app/root // ... export function Layout({ children }: { children: React.ReactNode }) { return ( ``` ## As a module import In your project entrypoint (e.g. `src/index`, `src/main`): ```jsx // src/index.jsx // must be imported before React and React DOM import { scan } from "react-scan"; import React from "react"; scan({ enabled: true, }); ``` If you want react-scan to also run in production, use the react-scan/all-environments import path ```diff - import { scan } from "react-scan"; + import { scan } from "react-scan/all-environments"; ``` > [!CAUTION] > React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it. ================================================ FILE: docs/installation/tanstack-start.md ================================================ # TanStack Router Guide ## As a script tag Add the script tag to your `` component at `app/routes/__root`. Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs. ```jsx // app/routes/__root import { Meta, Scripts } from "@tanstack/start"; // ... function RootDocument({ children }) { return ( ``` ## As a module import In your project entrypoint (e.g. `src/index`, `src/main`): ```jsx // src/index import { scan } from "react-scan"; // must be imported before React and React DOM import React from "react"; scan({ enabled: true, }); ``` If you want react-scan to also run in production, use the react-scan/all-environments import path ```diff - import { scan } from "react-scan"; + import { scan } from "react-scan/all-environments"; ``` > [!CAUTION] > React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it. ## Vite plugin TODO ## Preserving component names TODO ================================================ FILE: e2e/helpers.ts ================================================ import { type Page } from '@playwright/test'; export const FIXTURE_URL = '/?example=e2e-fixture'; export async function gotoFixture(page: Page): Promise { await page.goto(FIXTURE_URL); await page.waitForSelector('[data-testid="heading"]', { timeout: 10_000 }); // Wait for React Scan to boot and expose __REACT_SCAN__ await page.waitForFunction( () => typeof (window as any).__REACT_SCAN__?.ReactScanInternals !== 'undefined', { timeout: 15_000 }, ); // Install a render counter by patching the onRender option on the signal await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; const internals = (window as any).__REACT_SCAN__?.ReactScanInternals; if (internals?.options) { const prev = internals.options.value; const prevOnRender = prev.onRender; internals.options.value = { ...prev, onRender: (...args: any[]) => { (window as any).__E2E_RENDER_COUNT__++; if (prevOnRender) prevOnRender(...args); }, }; } }); // Wait for initial mount renders to settle then reset await page.waitForTimeout(500); await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; }); } export async function getRenderCount(page: Page): Promise { return page.evaluate(() => (window as any).__E2E_RENDER_COUNT__ ?? 0); } export async function waitForRenders( page: Page, timeout = 5000, ): Promise { const startCount = await getRenderCount(page); return page.evaluate( ({ start, t }) => { return new Promise((resolve) => { const check = () => { const current = (window as any).__E2E_RENDER_COUNT__ ?? 0; if (current > start) { resolve(current - start); return true; } return false; }; if (check()) return; const interval = setInterval(() => { if (check()) clearInterval(interval); }, 50); setTimeout(() => { clearInterval(interval); resolve(0); }, t); }); }, { start: startCount, t: timeout }, ); } export async function isReactScanActive(page: Page): Promise { return page.evaluate(() => { return typeof (window as any).__REACT_SCAN__ !== 'undefined'; }); } export async function hasShadowRoot(page: Page): Promise { return page.evaluate(() => { return document.getElementById('react-scan-root')?.shadowRoot != null; }); } ================================================ FILE: e2e/inspector.spec.ts ================================================ import { test, expect } from '@playwright/test'; import { gotoFixture } from './helpers'; test.describe('Inspector', () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); test('inspect state is available in React Scan internals', async ({ page }) => { const hasInspectState = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store) return false; const inspectState = scan.ReactScanInternals.Store.inspectState; return inspectState !== undefined && inspectState !== null; }); expect(hasInspectState).toBe(true); }); test('inspect state starts as inspect-off', async ({ page }) => { const kind = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; return scan?.ReactScanInternals?.Store?.inspectState?.value?.kind ?? null; }); expect(kind).toBe('inspect-off'); }); test('shadow DOM contains toolbar elements', async ({ page }) => { const elementCount = await page.evaluate(() => { const root = document.getElementById('react-scan-root'); return root?.shadowRoot?.querySelectorAll('*').length ?? 0; }); expect(elementCount).toBeGreaterThan(5); }); test('inspect state can be set programmatically', async ({ page }) => { const activated = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store?.inspectState) return false; scan.ReactScanInternals.Store.inspectState.value = { kind: 'focused', focusedDomElement: null }; return scan.ReactScanInternals.Store.inspectState.value.kind === 'focused'; }); expect(activated).toBe(true); }); }); ================================================ FILE: e2e/notifications.spec.ts ================================================ import { test, expect } from '@playwright/test'; import { gotoFixture } from './helpers'; test.describe('Notifications', () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); test('slow interaction is detected and recorded', async ({ page }) => { await page.click('[data-testid="trigger-slow"]'); await page.waitForTimeout(2000); const hasActiveStore = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store) return false; // Verify the notification system is wired up (interactionListeningForRenders is a function when active) return typeof scan.ReactScanInternals.Store.interactionListeningForRenders === 'function'; }); expect(hasActiveStore).toBe(true); }); test('notification system initializes with the toolbar', async ({ page }) => { const hasCanvas = await page.evaluate(() => { return document.querySelectorAll('canvas').length > 0; }); expect(hasCanvas).toBe(true); }); test('repeated slow interactions do not break the toolbar', async ({ page }) => { for (let i = 0; i < 3; i++) { await page.click('[data-testid="trigger-slow"]'); await page.waitForTimeout(500); } await page.waitForTimeout(2000); const shadowContent = await page.evaluate(() => { const root = document.getElementById('react-scan-root'); return root?.shadowRoot?.innerHTML ?? ''; }); expect(shadowContent.length).toBeGreaterThan(100); }); }); ================================================ FILE: e2e/outlines.spec.ts ================================================ import { test, expect, type Page } from '@playwright/test'; import { gotoFixture, getRenderCount } from './helpers'; async function clickAndCountRenders( page: Page, selector: string, waitMs = 1000, ): Promise { await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; }); await page.click(selector); await page.waitForTimeout(waitMs); return getRenderCount(page); } test.describe('Render Outlines', () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); test('state update triggers render tracking', async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="increment"]'); expect(count).toBeGreaterThan(0); }); test('rapid updates produce multiple tracked renders', async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="trigger-rapid"]', 2000); expect(count).toBeGreaterThan(5); }); test('outline canvas exists on the page', async ({ page }) => { const hasCanvas = await page.evaluate(() => { return document.querySelectorAll('canvas').length > 0; }); expect(hasCanvas).toBe(true); }); test('context change triggers render tracking', async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="toggle-theme"]'); expect(count).toBeGreaterThan(0); }); test('unstable props on memo components trigger render tracking', async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="trigger-unstable"]'); expect(count).toBeGreaterThan(0); }); test('render count accumulates with repeated clicks', async ({ page }) => { await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; }); await page.click('[data-testid="increment"]'); await page.waitForTimeout(300); const after1 = await getRenderCount(page); await page.click('[data-testid="increment"]'); await page.waitForTimeout(300); const after2 = await getRenderCount(page); await page.click('[data-testid="increment"]'); await page.waitForTimeout(300); const after3 = await getRenderCount(page); expect(after1).toBeGreaterThan(0); expect(after2).toBeGreaterThan(after1); expect(after3).toBeGreaterThan(after2); }); }); ================================================ FILE: e2e/toolbar.spec.ts ================================================ import { test, expect } from '@playwright/test'; import { gotoFixture, isReactScanActive, hasShadowRoot } from './helpers'; test.describe('Toolbar', () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); test('React Scan initializes and attaches to the page', async ({ page }) => { const active = await isReactScanActive(page); expect(active).toBe(true); }); test('React Scan internals are accessible', async ({ page }) => { const hasInternals = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; return ( scan?.ReactScanInternals !== undefined && scan.ReactScanInternals.options !== undefined && scan.ReactScanInternals.Store !== undefined ); }); expect(hasInternals).toBe(true); }); test('options are set correctly', async ({ page }) => { const options = await page.evaluate(() => { const scan = (window as any).__REACT_SCAN__; const opts = scan?.ReactScanInternals?.options?.value; if (!opts) return null; return { enabled: opts.enabled, dangerouslyForceRunInProduction: opts.dangerouslyForceRunInProduction, showToolbar: opts.showToolbar, }; }); expect(options).toEqual({ enabled: true, dangerouslyForceRunInProduction: true, showToolbar: true, }); }); test('shadow DOM root is created', async ({ page }) => { await page.waitForTimeout(1000); expect(await hasShadowRoot(page)).toBe(true); }); test('toolbar has content in shadow DOM', async ({ page }) => { await page.waitForTimeout(1000); const childCount = await page.evaluate(() => { const root = document.getElementById('react-scan-root'); return root?.shadowRoot?.children.length ?? 0; }); expect(childCount).toBeGreaterThan(0); }); test('toolbar persists across interactions', async ({ page }) => { await page.click('[data-testid="increment"]'); await page.waitForTimeout(500); const active = await isReactScanActive(page); expect(active).toBe(true); const options = await page.evaluate(() => { return (window as any).__REACT_SCAN__?.ReactScanInternals?.options?.value?.enabled; }); expect(options).toBe(true); }); }); ================================================ FILE: kitchen-sink/index.html ================================================ React Scan
================================================ FILE: kitchen-sink/package.json ================================================ { "name": "@react-scan/kitchen-sink", "type": "module", "private": true, "publishConfig": { "access": "restricted" }, "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "react-scan": "workspace:*", "tailwindcss": "^3.4.17" }, "devDependencies": { "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", "postcss": "^8.5.3", "typescript": "^5.7.3", "vite": "^6.3.0" } } ================================================ FILE: kitchen-sink/postcss.config.mjs ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: kitchen-sink/src/examples/e2e-fixture/index.tsx ================================================ import { useState, useContext, createContext, memo } from 'react'; import { scan, Store } from 'react-scan'; Store.isInIframe.value = false; scan({ enabled: true, dangerouslyForceRunInProduction: true, }); const ThemeContext = createContext('light'); function Counter(): JSX.Element { const [count, setCount] = useState(0); return (
{count}
); } function UnstableProps(): JSX.Element { const [tick, setTick] = useState(0); return (
{}} label="unstable" />
); } const MemoChild = memo(function MemoChild({ style, onClick, label, }: { style: { color: string }; onClick: () => void; label: string; }): JSX.Element { return (
MemoChild: {label}
); }); function ContextConsumer(): JSX.Element { const theme = useContext(ThemeContext); return
Theme: {theme}
; } function ThemeToggle(): JSX.Element { const [theme, setTheme] = useState('light'); return (
); } function SlowComponent(): JSX.Element { const [rendering, setRendering] = useState(false); const triggerSlowRender = () => { setRendering(true); const start = performance.now(); while (performance.now() - start < 100) { // block for 100ms to simulate slow render } setRendering(false); }; return (
{rendering ? 'Rendering...' : 'Idle'}
); } function RapidUpdater(): JSX.Element { const [count, setCount] = useState(0); const triggerRapid = () => { for (let i = 0; i < 50; i++) { setTimeout(() => setCount((c) => c + 1), i * 16); } }; return (
{count}
); } export default function E2EFixture(): JSX.Element { return (

React Scan E2E Fixture


Counter


Unstable Props (memo bypass)


Context


Slow Render


Rapid Updates

); } ================================================ FILE: kitchen-sink/src/examples/sierpinski/index.tsx ================================================ /** * Modified version of https://github.com/ryansolid/solid-sierpinski-triangle-demo **/ // import { Analytics } from '@vercel/analytics/react'; import { useEffect, useMemo, useState } from 'react'; import { scan, Store } from 'react-scan'; import './styles.css'; Store.isInIframe.value = false; scan({ enabled: true, dangerouslyForceRunInProduction: true, }); const TARGET = 50; const TriangleDemo = () => { const [elapsed, setElapsed] = useState(0); const [seconds, setSeconds] = useState(0); const scale = useMemo(() => { const e = (elapsed / 1000) % 10; return 1 + (e > 5 ? 10 - e : e) / 10; }, [elapsed]); useEffect(() => { const t = setInterval(() => setSeconds((s) => (s % 10) + 1), 1000); let f: number; const start = Date.now(); const update = () => { setElapsed(Date.now() - start); f = requestAnimationFrame(update); }; f = requestAnimationFrame(update); return () => { clearInterval(t); cancelAnimationFrame(f); }; }, []); return (
); }; interface SlowTriangleProps { x: number; y: number; s: number; seconds: number; } const SlowTriangle = ({ x, y, s, seconds }: SlowTriangleProps) => { s = s / 2; const slow = useMemo(() => { const e = performance.now() + 0.8; // Artificially long execution time. while (performance.now() < e) {} return seconds; }, [seconds]); return ( <> ); }; interface TriangleProps { x: number; y: number; s: number; seconds: number; } const Triangle = ({ x, y, s, seconds }: TriangleProps) => { if (s <= TARGET) { return ( ); } return ; }; interface DotProps { x: number; y: number; s: number; text: number; } const Dot = ({ x, y, s, text }: DotProps) => { const [hover, setHover] = useState(false); const onEnter = () => setHover(true); const onExit = () => setHover(false); return (
{hover ? '**' + text + '**' : text}
); }; export default function App(): JSX.Element { return ( <> {/* */} ); } ================================================ FILE: kitchen-sink/src/examples/sierpinski/styles.css ================================================ body { background: #fff; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.7; margin: 0; padding: 30px; } a { color: #4183c4; text-decoration: none; } a:hover { text-decoration: underline; } code { background-color: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; font-size: 12px; margin: 0 2px; padding: 0px 5px; } h1, h2, h3, h4 { font-weight: bold; margin: 0 0 15px; padding: 0; } h1 { border-bottom: 1px solid #ddd; font-size: 2.5em; font-weight: bold; margin: 0 0 15px; padding: 0; } h2 { border-bottom: 1px solid #eee; font-size: 2em; } h3 { font-size: 1.5em; } h4 { font-size: 1.2em; } p, ul { margin: 15px 0; } ul { padding-left: 30px; } .container { position: absolute; transform-origin: 0 0; left: 50%; top: 50%; width: 10px; height: 10px; background: #eee; } .dot { position: absolute; font: normal 15px sans-serif; text-align: center; cursor: pointer; } ================================================ FILE: kitchen-sink/src/examples/todo-list/index.tsx ================================================ import { useState } from 'react'; import { scan, Store } from 'react-scan'; import './styles.css'; Store.isInIframe.value = false; scan({ enabled: true, dangerouslyForceRunInProduction: true, }); interface TodoItem { id: number; message: string; done: boolean; } interface TodoListItemProps { setList: (action: (list: TodoItem[]) => TodoItem[]) => void; item: TodoItem; } function TodoListItem({ item, setList }: TodoListItemProps): JSX.Element { return (
{item.message}
); } interface TodoListFormProps { index: number; setIndex: (update: number) => void; setList: (action: (list: TodoItem[]) => TodoItem[]) => void; } function TodoListForm({ setList, index, setIndex, }: TodoListFormProps): JSX.Element { const [message, setMessage] = useState(''); return (
{ e.preventDefault(); setList(list => [ ...list, { done: false, message, id: index, }, ]); setIndex(index + 1); setMessage(''); }} > { setMessage((e.target as HTMLInputElement).value); }} />
); } function TodoList(): JSX.Element { const [list, setList] = useState([]); const [index, setIndex] = useState(0); return ( <>
{list.map(item => ( ))}
); } export default function App(): JSX.Element { return (

Todo List

); } ================================================ FILE: kitchen-sink/src/examples/todo-list/styles.css ================================================ * { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } button { margin: 0.5rem; padding: 0.5rem 1rem; border-radius: 0.5rem; outline: none; color: white; border-style: none; cursor: pointer; transition: background-color 200ms; } button:disabled { background-color: rgba(156, 163, 175, 255); color: rgba(229, 231, 235, 255); } .app { margin: 5%; } .todo-list-form { width: 100%; display: flex; align-items: center; justify-content: space-between; } .todo-list-form input { width: 100%; margin: 0.5rem; padding: 0.5rem; border-radius: 0.5rem; outline: none; border-width: 1px; border-color: black; border-style: solid; } .todo-list-form button:not(:disabled) { background-color: rgba(99, 102, 241, 255); } .todo-list-form button:not(:disabled):hover { background-color: rgba(67, 56, 202, 255); } .todo-list-form button:not(:disabled):active { background-color: rgba(79, 70, 229, 255); } .todo-item-toggle.pending { background-color: rgba(245, 158, 11, 255); } .todo-item-toggle.pending:hover { background-color: rgba(180, 83, 9, 255); } .todo-item-toggle.pending:active { background-color: rgba(217, 119, 6, 255); } .todo-item-toggle.complete { background-color: rgba(16, 185, 129, 255); } .todo-item-toggle.complete:hover { background-color: rgba(4, 120, 87, 255); } .todo-item-toggle.complete:active { background-color: rgba(5, 150, 105, 255); } .todo-item-delete:not(:disabled) { background-color: rgba(239, 68, 68, 255); } .todo-item-delete:not(:disabled):hover { background-color: rgba(185, 28, 28, 255); } .todo-item-delete:not(:disabled):active { background-color: rgba(220, 38, 38, 255); } .todo-item { margin: 0.5rem; padding: 0.5rem 1rem; border-radius: 0.5rem; display: flex; align-items: center; justify-content: space-between; transition: background-color 200ms; } .todo-item.loading { background-color: rgba(17, 24, 39, 255); color: white; } .todo-item.complete { background-color: rgba(209, 250, 229, 255); } .todo-item.pending { background-color: rgba(254, 243, 199, 255); } ================================================ FILE: kitchen-sink/src/index.css ================================================ body { margin: 0; } ================================================ FILE: kitchen-sink/src/index.tsx ================================================ import 'react-scan'; import { FC, lazy } from 'react'; import { createRoot } from 'react-dom/client'; import Home from './main'; import './index.css'; const examples = import.meta.glob }>( './examples/**/index.tsx', { eager: false, }, ); const root = document.getElementById('root'); if (root) { const embedded = new URLSearchParams(window.location.search); const page = embedded.get('example'); const target = `./examples/${page}/index.tsx`; if (page && target in examples) { const App = lazy(examples[target]); createRoot(root).render(); } else { createRoot(root).render(); } } ================================================ FILE: kitchen-sink/src/main.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; body { overflow: hidden; } ================================================ FILE: kitchen-sink/src/main.tsx ================================================ import { type JSX, useState } from 'react'; import './main.css'; interface Example { title: string; url: string; } const examples: Example[] = [ { title: 'Sierpinski Triangle', url: '/?example=sierpinski' }, { title: 'Todo List', url: '/?example=todo-list' }, ]; export default function Home(): JSX.Element { const [example, setExample] = useState(0); return (

react-scan

{/* content */}
{/* sidebar */} {examples.map((item, index) => ( ))}
{/* iframe */}