Million.js vs React Compiler vs React Scan: React Performance in 2026
Million.js vs React Compiler vs React Scan: React Performance in 2026
TL;DR
React performance has three distinct problems: unnecessary re-renders (most apps), slow rendering (complex UIs), and finding where the problem is (debugging). React Compiler (formerly React Forget) solves unnecessary re-renders automatically — it analyzes your code at build time and adds memoization where needed. Million.js makes rendering faster by replacing React's virtual DOM reconciler with a block-based DOM diffing algorithm. React Scan is the diagnostic tool — it highlights which components are re-rendering and why, making it the essential first step before optimizing. Start with React Scan to identify the problem. Use React Compiler first (it's automatic). Use Million.js for complex UIs that are still slow after compiler optimization.
Key Takeaways
- React Compiler is now stable — shipped with React 19, automatic memoization without
useMemo/useCallback - Million.js claims 70% faster rendering than React's default VDOM for data-heavy UIs
- React Scan uses no production overhead — dev-only tool that visualizes re-renders in real-time
- React Compiler eliminates most manual memoization —
memo(),useMemo(),useCallback()often no longer needed - Million.js
block()API wraps components for automatic DOM diffing optimization - React Scan GitHub stars: ~12k — fastest-growing React developer tool of 2025
- None of these conflict — React Compiler + Million.js can be used together
The Three React Performance Problems
Problem 1: Unnecessary Re-renders
Component re-renders when props/state haven't meaningfully changed
→ Solution: React Compiler (automatic), memo()/useMemo() (manual)
Problem 2: Slow Rendering
Component renders quickly but the DOM update itself is expensive
→ Solution: Million.js (faster diffing algorithm)
Problem 3: Finding the Problem
You don't know WHICH components are the bottleneck
→ Solution: React Scan (visual debugging), React DevTools Profiler
React Scan: Find Your Performance Problems First
React Scan visualizes React re-renders in real time — components flash when they render. It's the diagnostic step before any optimization work.
Installation
# Development only — zero production impact
npm install react-scan
<!-- Or via CDN for quick debugging of any React app -->
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
Setup
// app/layout.tsx (Next.js) or main.tsx (Vite)
// ONLY import in development
if (process.env.NODE_ENV === "development") {
const { scan } = await import("react-scan");
scan({
enabled: true,
log: false, // Console log re-renders
showToolbar: true, // Show floating toolbar
animationSpeed: "slow", // "slow" | "fast" | "off"
});
}
What React Scan Shows You
Every time a component re-renders → it flashes on screen
Green flash: Rendered because parent re-rendered (may be avoidable)
Yellow flash: Rendered because props changed (expected)
Red flash: Rendered frequently (performance problem)
Floating toolbar shows:
- Which components re-rendered this frame
- Re-render count
- Time spent rendering
React Scan CLI (No Code Changes)
# Analyze any React app without modifying code
npx react-scan http://localhost:3000
# Analyze production bundle
npx react-scan https://your-app.com
Identifying Expensive Components
// Before React Scan: you're guessing what's slow
function Dashboard() {
return (
<div>
<Header /> {/* Is this re-rendering unnecessarily? */}
<Sidebar /> {/* Is this expensive? */}
<DataTable /> {/* This has 1000 rows... */}
<UserPanel /> {/* Probably fine */}
</div>
);
}
// After React Scan: you KNOW DataTable re-renders on every keystroke
// because it's subscribed to a global state that changes on input
// → Add memo() or move state down
React Compiler: Automatic Memoization
React Compiler (officially stable in React 19) analyzes your components at build time and automatically inserts memoization — eliminating the need to manually write memo(), useMemo(), and useCallback().
Installation (Next.js)
npm install babel-plugin-react-compiler
// next.config.js
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;
Installation (Vite)
npm install babel-plugin-react-compiler @vitejs/plugin-react
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", {}],
],
},
}),
],
});
What React Compiler Does
// Code you write:
function ExpensiveList({ items, filter }) {
const filteredItems = items.filter((item) => item.name.includes(filter));
return (
<ul>
{filteredItems.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
}
// What React Compiler compiles to (conceptually):
const ExpensiveList = memo(function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(
() => items.filter((item) => item.name.includes(filter)),
[items, filter]
);
return (
<ul>
{filteredItems.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
});
React Compiler Rules of React Compliance
// React Compiler requires Rules of React — no violations
// ❌ Breaks compiler optimization: mutating state directly
function Bad() {
const [items, setItems] = useState([]);
items.push("new"); // Mutation — compiler gives up
setItems([...items]);
}
// ✅ Compiler-friendly: immutable updates
function Good() {
const [items, setItems] = useState([]);
setItems((prev) => [...prev, "new"]); // Immutable
}
// ❌ Breaks compiler: reading .current during render
function AlsoBad() {
const ref = useRef(0);
return <div>{ref.current}</div>; // Ref during render — unsafe
}
Opting Out of Compiler
// For components that can't be optimized (intentional side effects)
function MyComponent() {
"use no memo"; // Opt-out directive
// This component won't be auto-memoized
document.title = "My App"; // Side effect during render (bad practice, but opt-out covers it)
return <div>Content</div>;
}
Checking Compiler Effectiveness
# React Compiler eslint plugin shows what was/wasn't optimized
npm install eslint-plugin-react-compiler
# .eslintrc.js
module.exports = {
plugins: ["react-compiler"],
rules: {
"react-compiler/react-compiler": "error",
},
};
# Output: "Component X was not compiled because: [reason]"
Million.js: Faster Virtual DOM
Million.js replaces React's reconciliation algorithm for specific components. Instead of React's tree-diffing approach, Million uses a block-based static analysis that's significantly faster for dynamic-but-structurally-stable UIs (data tables, lists, dashboards).
Installation
npm install million
// vite.config.ts
import million from "million/compiler";
import react from "@vitejs/plugin-react";
export default {
plugins: [
million.vite({ auto: true }), // Automatic optimization
react(),
],
};
// next.config.js
const million = require("million/compiler");
module.exports = million.next({
reactCompiler: true, // Compatible with React Compiler
}, {
auto: { rsc: true }, // Server Components support
});
Manual block() API
import { block } from "million/react";
// Wrap heavy components with block() for faster rendering
const HeavyDataRow = block(function HeavyDataRow({
id,
name,
value,
status,
}: {
id: string;
name: string;
value: number;
status: "active" | "inactive";
}) {
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>{value.toFixed(2)}</td>
<td>
<span className={`badge ${status}`}>{status}</span>
</td>
</tr>
);
});
// Use in a data table — 10,000 rows renders fast
function DataTable({ data }: { data: Row[] }) {
return (
<table>
<tbody>
{data.map((row) => (
<HeavyDataRow key={row.id} {...row} />
))}
</tbody>
</table>
);
}
Automatic Mode
// With auto: true in config, Million analyzes and wraps compatible components
// No code changes needed — Million decides which components to optimize
// Million wraps components automatically when:
// - Component renders a predictable DOM structure
// - Props are simple values (strings, numbers, booleans)
// - No complex children that change structure dynamically
// Million skips components when:
// - Dynamic children that change type/structure
// - Complex context dependencies
// - Components it can't statically analyze
Performance Reality
Benchmark: Render 10,000 table rows with React (Vite, MacBook M2)
React default (no optimization): ~450ms initial, ~120ms updates
+ React Compiler: ~450ms initial, ~45ms updates (2.7x faster updates)
+ Million.js auto: ~180ms initial, ~25ms updates (4.8x faster than default)
+ React Compiler + Million.js: ~180ms initial, ~20ms updates (6x faster than default)
For typical 100-item lists:
Difference is imperceptible — don't optimize prematurely
Feature Comparison
| Feature | React Scan | React Compiler | Million.js |
|---|---|---|---|
| Purpose | Diagnose re-renders | Auto-memoization | Faster rendering |
| Production use | Dev only | ✅ Build time | ✅ Runtime |
| Code changes required | None | None (config only) | None (auto mode) or block() |
| React version req. | React 16+ | React 19 | React 16+ |
| Webpack/Vite support | Any | ✅ | ✅ |
| Next.js App Router | ✅ | ✅ | ✅ |
| Server Components | ✅ | ✅ | ✅ (RSC mode) |
| Conflicts with Compiler | N/A | N/A | ✅ Compatible |
| GitHub stars | 12k | (React repo) | 18k |
| Key limitation | Dev tool only | Rules of React required | Not all components benefit |
The Right Optimization Order
1. MEASURE with React Scan — identify which components re-render unnecessarily
2. ENABLE React Compiler — automatic memoization, zero code changes
3. RE-MEASURE with React Scan — check if problem components improved
4. ADD Million.js — for remaining slow renders (data tables, large lists)
5. PROFILE in DevTools — for remaining issues, use React DevTools Profiler
Common Mistakes:
❌ Optimizing without measuring (premature optimization)
❌ Adding useMemo/useCallback everywhere (React Compiler makes this redundant)
❌ Using Million.js for all components (not all benefit; some break)
❌ Ignoring React Compiler rules violations (silently disables optimization)
When to Use Each
Always use React Scan (in dev):
- It has zero production overhead
- It immediately shows you where performance problems are
- Install it before any optimization work
Use React Compiler if:
- You're on React 19 — just enable it, it's low-risk
- Your codebase follows Rules of React (no mutations, pure functions)
- You're spending time writing
useMemo/useCallbackmanually
Use Million.js if:
- Data tables, large lists, or dashboards with 500+ DOM nodes are slow
- React Compiler didn't solve your specific bottleneck
- You're building a data-intensive product (analytics, spreadsheets, editors)
Methodology
Performance benchmarks sourced from official Million.js benchmarks (million.dev/benchmarks), React Compiler team blog posts, and community reproduction benchmarks on GitHub. React Scan download statistics from npm (January 2026). GitHub star counts as of February 2026. Benchmark conditions specified where applicable — results vary significantly based on component complexity.
Related: React 19 New Features for what changed in the latest React version, or Zustand vs Jotai vs Valtio for state management patterns that affect performance.