react-scan vs why-did-you-render vs Million Lint 2026
react-scan vs why-did-you-render vs Million Lint 2026
TL;DR
React renders too much. Every tool in this comparison exists to tell you which components are the culprits, but each takes a fundamentally different approach. react-scan overlays real-time render highlights directly on your UI — a visual scanner that lights up components as they render. why-did-you-render patches React's internals and logs detailed prop/state change diffs to the console, showing you why a render happened. Million Lint is an IDE-level static + runtime analysis tool that marks slow components in your editor with severity indicators. In 2026, react-scan is the fastest way to spot what's rendering too much; why-did-you-render is best for diagnosing the specific cause; Million Lint catches issues before they reach production.
Key Takeaways
- react-scan: 50K+ weekly downloads, 9K GitHub stars — zero-config, visual overlay, no code changes required
- why-did-you-render: 1.3M weekly downloads, 11K GitHub stars — patches React to log prop/state diffs causing unnecessary renders
- Million Lint: 20K+ weekly downloads, 3K GitHub stars — VSCode extension + runtime analysis, IDE feedback before you run the app
- Setup friction: react-scan adds one script tag (or package import); why-did-you-render requires a setup file before React imports; Million Lint requires a Vite/Babel plugin
- Production use: react-scan and Million Lint are dev-only; why-did-you-render should never run in production
- Framework support: react-scan works with React 17+, Next.js, Remix; why-did-you-render supports React and React Native; Million Lint supports React + Vite/Next.js projects
The React Render Problem
React's virtual DOM reconciliation is fast, but unnecessary renders still cost CPU time, especially in large component trees. A parent re-rendering due to a state change often cascades — every child re-renders unless explicitly memoized with React.memo, useMemo, or useCallback.
The hard part is identifying which renders are unnecessary. The React DevTools Profiler can record sessions and show render timings, but it requires manual interaction and doesn't highlight components in real time. The three tools in this comparison give you faster, more targeted feedback loops.
react-scan
react-scan takes the visual approach — it draws colored overlays on components as they render in real time. Components that render frequently glow hotter (more orange/red). It requires zero code changes to your components.
Installation and Usage
Option 1: Script tag (zero config)
<!-- Add to your HTML head — works with any framework -->
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
Option 2: Package import
npm install react-scan
// app/layout.tsx (Next.js App Router)
import { ReactScan } from 'react-scan'
export default function RootLayout({ children }) {
return (
<html>
<body>
{process.env.NODE_ENV === 'development' && <ReactScan />}
{children}
</body>
</html>
)
}
Option 3: CLI for any running site
npx react-scan@latest http://localhost:3000
This last option is powerful — you can scan any React app without touching the source code, including production sites (read-only analysis).
What You See
react-scan draws a colored box around each component when it renders:
- Blue flash: component rendered once
- Yellow flash: rendered multiple times recently (possible issue)
- Red persistent glow: rendering continuously, likely a bug (missing dependency array, subscription leak)
A tooltip shows the component name, render count, and timestamp of the last render.
Configuration
import { ReactScan } from 'react-scan'
<ReactScan
enabled={process.env.NODE_ENV === 'development'}
log={false} // suppress console logs
showToolbar={true} // show the controls toolbar
animationSpeed="fast" // 'slow' | 'fast' | 'off'
dangerouslyForceRunInProduction={false} // never enable this
/>
Programmatic API
import { scan, getReport } from 'react-scan'
// Start scanning
scan({ enabled: true, log: true })
// Get a report after interaction
const report = getReport()
console.table(
Object.entries(report).map(([component, stats]) => ({
Component: component,
Renders: stats.count,
'Avg Time (ms)': stats.time.toFixed(2),
}))
)
This is useful for automated performance tests — scan during a scripted user interaction and assert that no component renders more than N times.
why-did-you-render
why-did-you-render patches React's internals to track prop and state changes that trigger renders. When a component re-renders but its props and state haven't meaningfully changed (e.g., a new object reference with identical values), it logs a detailed diff to the console.
Setup
why-did-you-render must be imported before React:
// src/wdyr.ts — must be first import in your app entry
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: false, // opt-in per component (recommended)
trackHooks: true, // track hook changes too
logOnDifferentValues: true, // log when values changed (not just referential)
collapseGroups: true, // cleaner console output
include: [/^Connect/], // regex to auto-track matching component names
})
}
// index.tsx or main.tsx
import './wdyr' // MUST be first
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
Opting Components In
// UserCard.tsx
import React from 'react'
interface UserCardProps {
user: { id: string; name: string; email: string }
onSelect: (id: string) => void
}
function UserCard({ user, onSelect }: UserCardProps) {
return (
<div onClick={() => onSelect(user.id)}>
{user.name} — {user.email}
</div>
)
}
// Enable WDYR tracking for this component
UserCard.whyDidYouRender = true
export default UserCard
Console Output
When UserCard renders unnecessarily:
🔴 UserCard re-rendered.
The props object changed but its values are all equal.
This could have been avoided with a hook like useMemo.
Prev props: { user: { id: "1", name: "Alice", email: "alice@..." } }
Next props: { user: { id: "1", name: "Alice", email: "alice@..." } }
Props diff: { user: "Object changed but values are equal" }
This tells you the exact cause: the parent is creating a new user object on every render instead of memoizing it. Fix: const user = useMemo(() => ({ id, name, email }), [id, name, email]) in the parent.
Tracking Hooks
whyDidYouRender(React, {
trackHooks: true,
})
// Now hook changes are logged:
// 🔴 UserList re-rendered via useSelector.
// The hook changed values: state.users changed.
// Prev: [...10 items]
// Next: [...10 items] (same items, different reference)
This is especially useful for Redux/Zustand selectors that return new object references on every store update even when the data hasn't changed.
Million Lint
Million Lint is an IDE extension and build-time plugin that analyzes your React components statically and at runtime to identify slow renders. It marks problem components directly in your editor with inline annotations.
Installation
npm install @million/lint
Vite config:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import MillionLint from '@million/lint'
export default defineConfig({
plugins: [
MillionLint.vite({ enabled: true }),
react(),
],
})
Next.js config:
// next.config.ts
import MillionLint from '@million/lint'
const nextConfig = {
// ... your config
}
export default MillionLint.next(nextConfig)
IDE Integration
The VSCode extension shows inline annotations:
// ProductList.tsx
// 🔴 Lint: ProductList renders 23ms on average — above 16ms threshold
function ProductList({ products, filters }) {
const filtered = products.filter(p => // 🟡 Inline: expensive filter on every render
filters.every(f => p.tags.includes(f))
)
return (
<div>
{filtered.map(product => (
<ProductCard key={product.id} product={product} /> // 🔴 ProductCard re-renders on every parent render
))}
</div>
)
}
Clicking an annotation opens a panel showing:
- Render frequency (renders/second)
- Average render duration
- Props that change most frequently
- Suggested fix (memoize, move state down, etc.)
Static Analysis
Million Lint performs static analysis even without running the app:
// ⚠️ Detected: inline object prop — creates new reference each render
<ProductCard style={{ padding: 16, margin: 8 }} />
// ⚠️ Detected: inline function prop — creates new reference each render
<Button onClick={() => handleClick(id)} />
// ✅ Suggested fix:
const buttonStyle = { padding: 16, margin: 8 }
const handleButtonClick = useCallback(() => handleClick(id), [id])
<ProductCard style={buttonStyle} />
<Button onClick={handleButtonClick} />
Feature Comparison
| Feature | react-scan | why-did-you-render | Million Lint |
|---|---|---|---|
| Setup complexity | Minimal (script tag) | Medium (setup file first) | Medium (plugin) |
| Visual overlay | ✅ Real-time highlights | ❌ | ❌ |
| Console diff output | ❌ | ✅ Detailed prop/state diffs | ❌ |
| IDE integration | ❌ | ❌ | ✅ VSCode annotations |
| Static analysis | ❌ | ❌ | ✅ Pre-runtime |
| Runtime analysis | ✅ | ✅ | ✅ |
| React Native | ❌ | ✅ | ❌ |
| Zero code changes | ✅ | ❌ | ❌ (plugin required) |
| Programmatic API | ✅ | ✅ | ✅ |
| Weekly downloads | ~50K | ~1.3M | ~20K |
When to Choose Each
Choose react-scan if:
- You want to quickly identify which components are rendering too much
- You're doing a first-pass audit of an unfamiliar codebase
- You want visual, real-time feedback during interactive testing
- You need to scan a production site without modifying source code (
npx react-scan url)
Choose why-did-you-render if:
- You know which component has a problem and want to understand why
- You need to track prop/state diffs to find unnecessary reference changes
- You're debugging Redux/Zustand selectors that trigger excessive renders
- You need React Native support
Choose Million Lint if:
- You want feedback before running the app — catch issues during development
- You prefer IDE-integrated workflow over browser tooling
- You're starting a new project and want performance guardrails from day one
- You want automated suggestions (memoize this, extract that callback)
A Typical Debugging Workflow
The tools work best in sequence:
-
react-scan — run it for 5 minutes while using your app. Look for components with persistent red glows or high render counts. You've identified the suspects.
-
why-did-you-render — opt in those specific suspect components. Interact with the UI and read the console. You now know exactly what prop/state is changing and whether it's a reference equality issue.
-
Fix — apply the appropriate optimization:
React.memo,useMemo,useCallback, move state closer to where it's used, or fix a selector that returns new references. -
Million Lint — add to your project to prevent regressions. It'll flag new violations as you write code, before they make it into a PR.
Methodology
- npm download data from npmjs.com registry API, March 2026
- react-scan: react-scan.com
- why-did-you-render: github.com/welldone-software/why-did-you-render
- Million Lint: million.dev/docs/lint
- Testing with React 19 on Next.js 15
See React performance libraries on PkgPulse.
Related: TanStack Query v5 vs SWR v3 vs RTK Query 2026 · tinybench vs mitata vs vitest bench JavaScript Benchmarking 2026