Skip to main content

Guide

Million.js vs React Compiler vs React Scan 2026

Million.js vs React Compiler vs React Scan compared for React performance optimization. Automatic memoization, re-render detection, virtualization, and.

·PkgPulse Team·
0

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)

React Compiler Adoption Risks and Compatibility

React Compiler's automatic memoization requires that your components follow the Rules of React strictly — no state mutations, no reading refs during render, no side effects outside useEffect. In practice, many real-world codebases have small violations that prevent the compiler from optimizing specific components. The compiler's eslint plugin surfaces these violations with explanations, but fixing them can require refactoring patterns that developers didn't realize were invalid. Common violations include reading ref.current during the render function, mutating state objects directly rather than using immutable update patterns, and relying on render side effects. Components that the compiler cannot safely optimize are marked with "use no memo" internally and left unoptimized — you don't get worse performance, but you don't get the compiler's benefit either. Teams adopting React Compiler in 2026 should audit their codebase with the eslint plugin before enabling the compiler globally, then enable it for new files first while fixing violations in existing code incrementally.

Million.js Limitations and Component Compatibility

Million.js's block-based DOM diffing excels at structurally stable UIs — tables, lists, dashboards where the DOM structure doesn't change between renders and only data values update. It performs poorly (or breaks entirely) for components with highly dynamic structure: conditional rendering that changes the number or type of DOM elements, children passed as props with variable length, and components that use context heavily. Million's automatic mode (auto: true) applies static analysis to determine which components are blockifiable and skips those it cannot safely transform. Manual block() wrapping gives you explicit control but requires you to understand the constraint: props must be primitive values or simple objects, not functions or React elements. The practical deployment pattern in 2026 is to enable auto mode and measure — Million reports which components it optimized in development mode. Teams building analytics dashboards with thousands of table cells typically see the most dramatic improvements; teams building form-heavy applications may see minimal benefit.

React DevTools Profiler vs React Scan

React Scan and the built-in React DevTools Profiler complement rather than replace each other. React Scan provides always-on visual feedback during development — every re-render is visible as a flash without manually starting a profiling session. The DevTools Profiler provides deeper analysis: flame graphs showing render time for every component, ranked views of most expensive components, and the ability to see which specific props or state caused a re-render. The workflow that emerges in practice is: use React Scan to notice which components are re-rendering excessively during normal usage, then open the DevTools Profiler for specific interactions to understand the timing and causation. React Scan's CLI mode (npx react-scan https://your-app.com) is particularly useful for performance reviews of production builds — it can identify re-render patterns in the minified production bundle without requiring development mode.

TypeScript and Build Tool Integration

Both React Compiler and Million.js integrate at the build tool level via Babel plugins, which means they apply to all TypeScript and JSX code that passes through the bundler. React Compiler's Babel plugin is production-safe and recommended for both development and production builds. Million.js's compiler can be configured to run only on specific directories or file patterns, which is useful for large monorepos where you want to optimize specific packages. For Turbopack (Next.js 15+ default bundler), React Compiler has experimental support but Million.js's Vite/webpack plugin is not yet compatible — teams using Turbopack must fall back to webpack for Million.js optimization or wait for Turbopack compatibility. SWC-based builds (used by default in Next.js without Million.js) are compatible with React Compiler but Million.js's transform requires Babel, which means enabling the Babel transform option in next.config.js when using Million.js with Next.js.

Production Monitoring and Continuous Performance

The performance optimization workflow doesn't end at deployment — production React applications regress over time as new features add components and state. React Scan has a programmatic API that can report re-render metrics to your analytics infrastructure: tracking which components re-render most frequently in production (sampled, not on every render) can surface regressions before they become user-facing performance problems. Million.js's optimization is static — components blockified at build time don't change in production. React Compiler's memoization is also static and applied at build time. The practical implication is that performance improvements from both tools are locked in at each deployment; regressions come from new code that the compiler can't optimize or that isn't covered by Million.js's block pattern. Integrating performance budgets into your CI pipeline — running Playwright-based Lighthouse tests against representative pages — catches these regressions before they ship to users.

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.

See also: React vs Vue and React vs Svelte

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.