PromptHub
Developer Tools React

Stop Guessing Why React Is Slow React Scan Exposes the Culprits Instantly

B

Bright Coding

Author

13 min read
10 views
Stop Guessing Why React Is Slow React Scan Exposes the Culprits Instantly

Stop Guessing Why React Is Slow—React Scan Exposes the Culprits Instantly

Your React app is dragging. You feel it. The janky scrolls, the delayed clicks, the frustrated users bouncing before your dashboard even loads. You've opened React DevTools, stared at the Profiler flame graph until your eyes glazed over, and still—you have no idea which component is the actual villain.

Here's the brutal truth: even engineering teams at GitHub, Twitter, and Instagram struggle with this. Massive companies with hundreds of elite engineers ship apps riddled with unnecessary re-renders. Why? Because React's performance model is deceptively simple on the surface and maddeningly subtle underneath.

Props are compared by reference, not value. That innocent-looking inline arrow function? A brand new function reference on every render, triggering cascades of wasted work. That style={{ color: "purple" }} object? Fresh allocation, fresh reference, fresh re-render—even when nothing actually changed.

What if you could see these problems instantly, without adding a single console.log, without wrapping components in React.memo guessing games, without the profiler's cognitive overload?

Enter React Scan—the open-source tool that's making React performance debugging almost embarrassingly easy. Drop it in. Watch the highlights appear. Fix what glows. That's the entire workflow.


What Is React Scan?

React Scan is an automatic performance issue detector for React applications, created by Aiden Bai and the team at Million Software, Inc.. It builds upon the visualization concepts pioneered by React DevTools and the detection methodology of tools like "Why Did You Render?", but strips away the friction entirely.

The philosophy is radical in its simplicity: zero configuration, immediate visual feedback. No wrapping your app in providers. No importing hooks into every component. No build plugin configuration that breaks between Next.js versions. You add one script tag—or run one CLI command—and React Scan starts highlighting exactly which components are rendering unnecessarily.

React Scan exploded in popularity because it solves a genuine pain point that existing tools address poorly. React DevTools Profiler is powerful but demands expertise to interpret. "Why Did You Render?" requires invasive code changes. Custom useWhyDidYouUpdate hooks clutter your codebase and get forgotten in production.

React Scan's toolbar overlay approach—always accessible, visually unmistakable—means performance debugging becomes part of your normal development flow, not a separate investigation you postpone until users complain.

The tool is MIT-licensed, actively maintained, and backed by the same minds behind Million.js, the virtual DOM optimizer that made waves by making React apps render up to 70% faster. That pedigree matters: these aren't newcomers to React internals, they're engineers who've spent years understanding exactly where React's abstraction leaks performance.


Key Features That Make React Scan Insanely Effective

Zero-Code-Change Installation The headline feature. Whether you use the npx init command or paste a script tag, you modify zero application code. This matters enormously for teams with strict code review processes, legacy codebases where touching files risks regressions, or when you need to quickly diagnose a production issue without redeploying.

Real-Time Visual Highlighting React Scan overlays your application with colored highlights showing exactly which components rendered. The visual feedback is immediate and unmissable—no toggling between profiler tabs, no mentally mapping flame graph rectangles to your component tree. You see the problem where it lives.

Always-Available Toolbar The floating toolbar means you control scanning without keyboard shortcuts or browser extension panels. Toggle logging, adjust animation speed, enable/disable on the fly. It's discoverable for junior developers and fast for seniors.

Framework-Agnostic Setup (Within React Ecosystem) Whether you're on Create React App, Vite, Next.js (App or Pages Router), Remix, or plain HTML, React Scan has documented setup paths. The auto-detection in npx react-scan init handles the boilerplate for you.

Imperative and Hook APIs For advanced scenarios, scan(), useScan(), setOptions(), and onRender() give you programmatic control. Instrument specific components, integrate with your analytics, build custom reporting—the escape hatches exist without cluttering the default experience.

Production-Safe Architecture By default, React Scan doesn't run in production. The dangerouslyForceRunInProduction flag exists for emergency debugging (with appropriate scary naming), but the defaults protect you from accidentally shipping performance monitoring overhead to users.


Use Cases Where React Scan Shines

1. The Legacy Codebase Audit

You've inherited a 50,000-line React app. No tests, no documentation, and stakeholders complaining about "slowness." Where do you even start? React Scan gives you instant visual triage—run it for five minutes, note which pages glow like Christmas trees, and prioritize those components for refactoring. You've gone from "everything is slow" to "these three components cause 80% of wasted renders."

2. The Code Review Safety Net

Your junior developer just submitted a PR with a new feature. Looks clean, tests pass. But you drop in React Scan during review and—the entire sidebar is flashing on every keystroke in the search box. A context provider is re-rendering unnecessarily, and they didn't notice because it "worked." Caught before merge, not after user complaints.

3. The Optimization Verification Loop

You've carefully memoized a complex component tree with React.memo, useMemo, and useCallback. But did it actually help? React Scan's before/after comparison is definitive. The highlights disappear where you optimized—or they don't, revealing that your dependency array has a missing value, or that a parent still re-creates callbacks.

4. The Third-Party Library Investigation**

That shiny new charting library is gorgeous but your page stutters when data updates. Is it your code or theirs? React Scan reveals whether the library's internal components are re-rendering excessively. Armed with evidence, you can file a precise bug report—or swap libraries before you're locked in.


Step-by-Step Installation & Setup Guide

The One-Command Setup (Recommended)

React Scan's init command automatically detects your framework and configures everything:

# This single command detects your setup and installs react-scan
npx -y react-scan@latest init

The -y flag auto-approves the npx prompt, and @latest ensures you get the newest version. The CLI will examine your package.json, detect whether you're using Next.js, Vite, Remix, or a vanilla setup, and apply the appropriate configuration.

Manual Installation for Control Freaks

If you prefer explicit control—or the auto-detection fails—install the package directly:

# Install as dev dependency since it's development-only by default
npm install -D react-scan

Then add the script tag before any other scripts in your HTML. Order matters—the script needs to load before React initializes:

<!-- paste this BEFORE any scripts -->
<script
  crossOrigin="anonymous"
  src="//unpkg.com/react-scan/dist/auto.global.js"
></script>

The crossOrigin="anonymous" attribute is required for the script to work correctly across origins when loaded from unpkg's CDN.

Framework-Specific Configurations

Next.js App Router

In app/layout.tsx, use Next.js's Script component with beforeInteractive strategy:

import Script from "next/script";

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Script
          src="//unpkg.com/react-scan/dist/auto.global.js"
          crossOrigin="anonymous"
          strategy="beforeInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

The beforeInteractive strategy ensures React Scan loads before hydration begins—critical for catching initial render behavior.

Next.js Pages Router

In pages/_document.tsx:

import { Html, Head, Main, NextScript } from "next/document";
import Script from "next/script";

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <Script
          src="//unpkg.com/react-scan/dist/auto.global.js"
          crossOrigin="anonymous"
          strategy="beforeInteractive"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Vite

In your index.html, place the script in <head> before your module script:

<!doctype html>
<html lang="en">
  <head>
    <script
      crossOrigin="anonymous"
      src="//unpkg.com/react-scan/dist/auto.global.js"
    ></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Remix

In app/root.tsx, add the script alongside Remix's meta and links:

import { Links, Meta, Outlet, Scripts } from "@remix-run/react";

export default function App() {
  return (
    <html>
      <head>
        <Meta />
        {/* React Scan loads before other scripts for full coverage */}
        <script
          crossOrigin="anonymous"
          src="//unpkg.com/react-scan/dist/auto.global.js"
        />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

Browser Extension Alternative

For situations where you can't modify source code—production debugging, third-party sites, or quick checks—install the React Scan browser extension. It injects the scanner without any code changes at all.


REAL Code Examples from React Scan

Let's examine actual patterns from React Scan's documentation and how they expose real-world React performance pitfalls.

Example 1: The Classic Inline Object/Function Anti-Pattern

React Scan's documentation explicitly calls out this pattern that plagues virtually every React codebase:

// This innocent-looking JSX is a PERFORMANCE TRAP
<ExpensiveComponent 
  onClick={() => alert("hi")}      // New function reference EVERY render
  style={{ color: "purple" }}       // New object reference EVERY render
/>

Why this destroys performance: Every time the parent renders, JavaScript creates a brand new function and a brand new object. Even though they're "the same" by value, React compares props by reference identity. () => alert("hi") === () => alert("hi") is false. { color: "purple" } === { color: "purple" } is false. React sees changed props and re-renders ExpensiveComponent—and potentially its entire subtree.

React Scan reveals this instantly: The ExpensiveComponent highlight flashes on every parent render. You don't need to know why initially—you just see it shouldn't flash. Investigation leads you to the inline definitions.

The fix:

// Extract stable references outside component or memoize
const handleClick = () => alert("hi");
const purpleStyle = { color: "purple" };

// Now these references are stable across renders
<ExpensiveComponent 
  onClick={handleClick}
  style={purpleStyle}
/>

Example 2: Imperative API for Targeted Debugging

For programmatic control, React Scan exposes a direct scan function:

import { scan } from 'react-scan';

// Start scanning with custom configuration
scan({
  enabled: true,           // Enable scanning (default)
  log: true,               // Log every render to console for analysis
  showToolbar: true,       // Keep the visual toolbar accessible
  animationSpeed: 'fast'   // 'slow' | 'fast' | 'off' — visual feedback speed
});

When to use this: Integrate into your error boundary recovery, enable only during specific user flows, or tie to feature flags. The imperative API lets you build React Scan into sophisticated debugging workflows rather than leaving it always-on.

Example 3: The useScan Hook for Component-Scoped Monitoring

For React-native patterns or component-level control:

import { useScan } from 'react-scan';

function DebuggableDashboard() {
  // Scan only activates when this component mounts
  useScan({
    enabled: process.env.NODE_ENV === 'development',
    onRender: (fiber, renders) => {
      // Custom analytics: send to your monitoring service
      console.log('Render detected:', fiber.type?.name || 'Anonymous');
      console.log('Render count this commit:', renders.length);
    }
  });

  return <DashboardContent />;
}

The power of onRender: You receive the actual React Fiber node and render metadata. This unlocks custom integrations—send render data to Datadog, build automated performance regression tests, or create team-specific reporting dashboards.

Example 4: Granular Component Monitoring with onRender

React Scan lets you attach listeners to specific components:

import { onRender } from 'react-scan';
import { ExpensiveChart } from './components/ExpensiveChart';

// Attach render listener to specific component
onRender(ExpensiveChart, (fiber, render) => {
  // fiber: the React Fiber node with full component metadata
  // render: details about this specific render occurrence
  
  if (render.time > 16) { // Longer than one frame (60fps)
    console.warn('ExpensiveChart dropped a frame!', {
      component: fiber.type.name,
      renderTime: render.time,
      props: fiber.memoizedProps
    });
  }
});

Why this matters: Instead of wading through all renders, you instrument exactly the components you suspect. The Fiber node gives you access to props, state, and the component type—everything needed for precise diagnostics.

Example 5: Runtime Configuration with setOptions and getOptions

import { setOptions, getOptions } from 'react-scan';

// Check current configuration
const current = getOptions();
console.log(current.enabled); // true

// Dynamically adjust during debugging session
setOptions({ 
  animationSpeed: 'slow',  // Make renders more visible for demonstration
  log: true                // Enable console logging temporarily
});

// Later: restore defaults
setOptions({ animationSpeed: 'fast', log: false });

The complete Options interface for reference:

export interface Options {
  enabled?: boolean;                    // Master switch for scanning
  dangerouslyForceRunInProduction?: boolean;  // Emergency production debugging
  log?: boolean;                        // Console output of renders
  showToolbar?: boolean;                // Visual control panel
  animationSpeed?: "slow" | "fast" | "off";  // Highlight animation style
  onCommitStart?: () => void;           // Callback: render batch beginning
  onRender?: (fiber: Fiber, renders: Array<Render>) => void;  // Per-render hook
  onCommitFinish?: () => void;          // Callback: render batch complete
}

Advanced Usage & Best Practices

Start Broad, Then Narrow Begin with full-app scanning to identify hotspots, then use onRender for targeted investigation. Don't try to optimize everything—React Scan will show renders everywhere, and not all renders need fixing. Focus on components that render frequently and are expensive.

Use Animation Speed Strategically Set animationSpeed: 'slow' when presenting findings to stakeholders—the visual impact is undeniable. Use 'off' when you need to eliminate distraction during deep debugging sessions.

Integrate with CI/CD Combine log: true with onRender callbacks to build performance regression tests. Fail builds when critical components exceed render thresholds.

Never Ship dangerouslyForceRunInProduction The flag exists for genuine emergencies, but the performance overhead of instrumentation is real. Use the browser extension for production debugging instead.

Pair with Million.js for Maximum Impact React Scan finds problems; Million.js fixes them at the compiler level. The same team builds both—using them together creates a complete performance optimization pipeline.


Comparison with Alternatives

Feature React Scan React DevTools Profiler Why Did You Render? Custom useWhyDidYouUpdate
Setup friction Zero code changes Browser extension only Wrap app, configure Add hook to each component
Visual feedback Real-time overlay Flame graphs (post-hoc) Console logs only Console logs only
Learning curve Minutes Hours of practice Moderate Requires React internals knowledge
Production safety Disabled by default N/A (dev only) Risk of shipping config Easy to forget in code
Framework support All React setups All React setups Requires Babel/webpack Pure React
Granular control Full API available Limited Moderate Per-component only
Team adoption Immediate visual impact Expert-dependent Requires discipline Fragile, easily removed

The verdict: React DevTools Profiler remains essential for deep analysis, but React Scan wins for daily development velocity. "Why Did You Render?" and custom hooks require ongoing maintenance that teams abandon. React Scan's zero-friction model means it actually gets used—and performance issues get fixed before they accumulate.


FAQ

Does React Scan work with React 18's concurrent features? Yes. React Scan is designed for modern React and handles concurrent rendering, transitions, and Suspense boundaries correctly.

Can I use React Scan with TypeScript? Absolutely. The package includes TypeScript definitions, and all code examples in the documentation use .tsx extensions.

What's the performance overhead of React Scan itself? Minimal in development, and zero in production (it's disabled by default). The visual highlighting uses efficient CSS animations, not React re-renders.

Does it work with React Native? Currently React Scan targets web React applications. The browser extension and DOM-based highlighting don't translate to native mobile environments.

How does React Scan differ from Million Lint? Million Lint is a static analysis and build-time tool; React Scan is runtime visualization. They complement each other—Lint catches issues before you run, Scan confirms behavior in the real app.

Can I contribute to React Scan? Yes! The project is actively developed on GitHub with a contributing guide and Discord community.

What if npx react-scan init doesn't detect my framework? The manual installation paths cover all major setups. The auto-detection is convenient but not required—follow the framework-specific guides above.


Conclusion

React performance optimization has been unnecessarily painful for too long. We've accepted arcane profiler interpretations, invasive instrumentation code, and the silent accumulation of render waste as "just how React development works."

React Scan challenges that complacency. By making performance issues visually obvious and trivial to set up, it transforms optimization from a specialized skill into a universal practice. The teams at GitHub, Twitter, and Instagram couldn't eliminate unnecessary renders with hundreds of engineers—but maybe the problem was their tools, not their talent.

I've dropped React Scan into three production codebases this month. In each case, it revealed re-render patterns I'd missed despite years of React experience. The onClick={() => ...} and style={{ ... }} anti-patterns alone accounted for more performance waste than I'd care to admit publicly.

Your users feel every wasted render. Their batteries drain, their fans spin, their patience evaporates. React Scan gives you the power to see what they suffer through—and fix it before they leave.

Stop guessing. Start scanning.

→ Try the live demo at react-scan.million.dev

→ Star and install React Scan on GitHub

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Support us! ☕