Skip to main content

Million.js vs React Compiler vs React Scan: React Performance in 2026

·PkgPulse Team

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 memoizationmemo(), 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

FeatureReact ScanReact CompilerMillion.js
PurposeDiagnose re-rendersAuto-memoizationFaster rendering
Production useDev only✅ Build time✅ Runtime
Code changes requiredNoneNone (config only)None (auto mode) or block()
React version req.React 16+React 19React 16+
Webpack/Vite supportAny
Next.js App Router
Server Components✅ (RSC mode)
Conflicts with CompilerN/AN/A✅ Compatible
GitHub stars12k(React repo)18k
Key limitationDev tool onlyRules of React requiredNot 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/useCallback manually

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.