Million.js: Hyper-Optimized Virtual DOM for React (2026)

Million.js is a drop-in compiler and runtime that makes React components up to 70% faster by replacing React's diffing algorithm with a fine-grained, block-based virtual DOM. Instead of diffing the entire component tree on every render, Million identifies static structure at compile time and only patches the dynamic parts at runtime — the same idea behind Svelte and Solid.js, but layered on top of React so you keep the full ecosystem. This guide covers how it works, the block() API, the automatic compiler, Million Lint, and when it actually helps.

How Million.js Works

// React's virtual DOM diffing (default):
// On every render, React creates a new vDOM tree and diffs the whole subtree.
// Even if only one value changed, React walks every node to find what's different.
//
// Million's block-based vDOM:
// At compile time, Million statically analyzes your JSX and splits it into:
//   - Static "holes" — fixed structure that never changes (div, class, layout)
//   - Dynamic "slots" — the actual values that can change (text, classNames, handlers)
//
// At runtime, Million only updates the dynamic slots — skipping the diff entirely.
// This is O(slots) instead of O(tree size) — a huge win for large, mostly-static UIs.
//
// Before Million (React default):
//   render() → new vDOM tree → diff against old tree → patch DOM
//
// After Million:
//   render() → extract new slot values → patch only changed slots in DOM
//   (No diff. No full tree walk. Direct DOM updates.)
//
// Result: 70%+ faster renders on components with large static structure
// and a small number of frequently-changing values (dashboards, tables, lists).

Setup

npm install million

# Vite
# vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import million from 'million/compiler'

export default defineConfig({
  plugins: [
    million.vite({ auto: true }),   // auto mode: compiler wraps components automatically
    react(),
    // Million plugin must come BEFORE the React plugin
  ],
})

# Next.js
# next.config.mjs
import million from 'million/compiler'

const nextConfig = { reactStrictMode: true }
export default million.next(nextConfig, { auto: true })

# Webpack / CRA
# webpack.config.js
const MillionCompiler = require('million/compiler')
module.exports = {
  plugins: [MillionCompiler.webpack({ auto: true })]
}

The block() API

import { block } from 'million/react'

// Wrap a component with block() to opt it into Million's fast vDOM.
// The component must follow one rule: props must be serializable primitives
// (string, number, boolean) — not functions or complex objects as direct props.

interface ProductCardProps {
  name: string
  price: number
  inStock: boolean
  category: string
  imageUrl: string
}

// Regular component — block() wraps it
function ProductCardBase({ name, price, inStock, category, imageUrl }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={imageUrl} alt={name} />
      <span className="category">{category}</span>
      <h3>{name}</h3>
      <p className={inStock ? 'in-stock' : 'out-of-stock'}>
        {inStock ? 'In Stock' : 'Out of Stock'}
      </p>
      <span className="price">${price.toFixed(2)}</span>
    </div>
  )
}

// block() returns a new component that uses Million's fast rendering
export const ProductCard = block(ProductCardBase)

// Use it exactly like a normal React component
function ProductGrid({ products }: { products: ProductCardProps[] }) {
  return (
    <div className="grid">
      {products.map(p => (
        <ProductCard key={p.name} {...p} />
      ))}
    </div>
  )
}

// What Million does at compile time:
// It sees that <div className="product-card"> never changes,
// <img src={imageUrl}> has one dynamic attribute,
// {name} is a dynamic text node, etc.
// It builds a static template once, then on each render only patches
// the values for imageUrl, category, name, inStock, price.

Automatic Compiler Mode

// With auto: true in the plugin config, Million's compiler automatically
// decides which components to wrap with block() — you don't call block() manually.
//
// The compiler analyzes every component and wraps it if:
// - The component returns JSX (not null, string, or array)
// - Props are serializable (no function props passed directly into JSX)
// - The component doesn't use hooks that conflict with Million's model
//
// Opt a specific component OUT of auto mode:
import { noAuto } from 'million/react'

// @million ignore
function ComplexDynamicComponent() {
  // This component has dynamic structure (conditional JSX shape changes)
  // Million can't optimize it safely — exclude it
  return isLoading ? <Spinner /> : <Dashboard />
}

// Or use the decorator comment:
// @million ignore
// function MyComponent() { ... }

// Opt a specific file out entirely:
// Add to the top of the file: /* @million disable */

// Fine-tuning auto mode in plugin config:
million.vite({
  auto: {
    threshold: 0.1,   // Only wrap components where >10% of nodes are dynamic (default 0.1)
    skip: ['VideoPlayer', 'Canvas3D'],   // Component names to skip
  }
})

Million Lint

npm install --save-dev @million/lint

# .eslintrc.cjs
module.exports = {
  plugins: ['@million/lint'],
  rules: {
    '@million/lint/rules-of-hooks': 'warn',  // Flag patterns that prevent optimization
  },
}

// Million Lint is a VS Code extension + ESLint plugin that:
// 1. Shows which components are being optimized by Million
// 2. Warns when a component can't be optimized and explains why
// 3. Gives a "slowness score" based on renders per second
//
// Common Lint warnings and fixes:
//
// Warning: "Component passes function as prop directly into JSX"
// ❌ Bad — function prop prevents block() from working:
function ParentBad() {
  return <ProductCard onClick={() => buy(id)} />
}
// ✅ Fix — lift the handler outside or use useCallback at the call site:
const handleBuy = useCallback(() => buy(id), [id])
return <ProductCard onClick={handleBuy} />

// Warning: "Component returns different JSX shapes based on state"
// ❌ Bad — dynamic structure:
function BadCard({ isExpanded }: { isExpanded: boolean }) {
  return isExpanded
    ? <div><header/><p/><footer/></div>
    : <div><header/></div>
}
// ✅ Fix — keep structure static, toggle content inside:
function GoodCard({ isExpanded }: { isExpanded: boolean }) {
  return (
    <div>
      <header />
      {isExpanded && <p />}
      {isExpanded && <footer />}
    </div>
  )
}

For — Optimized List Rendering

import { For } from 'million/react'

// Million's <For> component is an optimized replacement for .map()
// It uses a keyed list diffing algorithm that avoids re-rendering unchanged items
// — similar to Solid.js <For> and Vue's v-for with :key

interface User {
  id: string
  name: string
  role: string
  avatarUrl: string
}

function UserList({ users }: { users: User[] }) {
  return (
    <For each={users} as="ul">
      {(user) => (
        // Each item rendered with block-based optimization
        <li key={user.id}>
          <img src={user.avatarUrl} alt={user.name} width={32} height={32} />
          <span>{user.name}</span>
          <span>{user.role}</span>
        </li>
      )}
    </For>
  )
}

// For large lists (1000+ items), combine with virtualization:
import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualUserList({ users }: { users: User[] }) {
  const parentRef = useRef<HTMLDivElement>(null)
  const virtualizer = useVirtualizer({
    count: users.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 56,
  })

  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(item => {
          const user = users[item.index]
          return (
            <div key={user.id} style={{ position: 'absolute', top: item.start, width: '100%' }}>
              <UserRow user={user} />   {/* UserRow is wrapped with block() */}
            </div>
          )
        })}
      </div>
    </div>
  )
}

When to Use Million

// Million gives the biggest wins for:
// ✅ Data tables with many rows and frequently-updating cells
// ✅ Real-time dashboards (stock tickers, analytics, monitoring)
// ✅ Large lists (product grids, feed items) that re-render often
// ✅ Components with large static structure and small dynamic surface area

// Million gives little or no benefit for:
// ❌ Components that rarely re-render (most UI — buttons, forms, layouts)
// ❌ Components with dynamic JSX structure (conditional different shapes)
// ❌ Components that render mostly other components (layout wrappers)
// ❌ Highly interactive components where the bottleneck is event handling

// Benchmark approach — measure before and after:
import { Profiler } from 'react'

function onRender(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
) {
  console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`)
}

function App() {
  return (
    <Profiler id="ProductGrid" onRender={onRender}>
      <ProductGrid products={products} />
    </Profiler>
  )
}

// Real-world benchmark results (approximate):
// Component type          | React default | With Million | Speedup
// ----------------------- | ------------- | ------------ | -------
// 1000-row data table     |    28ms       |    8ms       |  3.5x
// Real-time stock ticker  |    12ms       |    4ms       |  3x
// Static card grid (once) |     3ms       |    3ms       |  ~1x (no gain)
// Form with inputs        |     2ms       |    2ms       |  ~1x (no gain)

Limitations

// 1. Props must be "holes" — serializable leaf values, not components or render props.
// ❌ Won't optimize:
block(({ renderHeader }: { renderHeader: () => ReactNode }) => (
  <div>{renderHeader()}</div>
))

// 2. Hooks that manipulate the DOM directly (useRef + imperative DOM) can conflict.
// ❌ block() + imperative ref manipulation may cause issues.
// ✅ Use noAuto / skip these components in auto mode.

// 3. Context consumers re-render normally — Million doesn't optimize context reads.
// For context-heavy components, Million has no advantage.

// 4. React Server Components — block() only works in client components.
// Don't apply block() to RSCs; they run on the server and have no vDOM.

// 5. Fragments as root — block() requires a single root element.
// ❌ Won't work:
block(() => <><span /><span /></>)
// ✅ Wrap in a div:
block(() => <div><span /><span /></div>)

// 6. Dynamic keys in lists inside blocks — use <For> instead of .map() inside blocks.

// React Forget / React Compiler (official React optimizer):
// React 19 ships with the React Compiler (formerly React Forget) which automatically
// memoizes components. For many apps, the React Compiler reduces the need for Million.
// Use Million alongside React Compiler for maximum benefit on render-heavy components.
// They target different bottlenecks: React Compiler reduces unnecessary renders;
// Million makes the renders that DO happen faster.
Start with profiling: Don't add Million to a new project and assume it will help. Use the React Profiler or browser DevTools Performance tab to find components with high actualDuration. Apply block() only to the hot components. In most React apps, a handful of components account for the majority of render time — optimize those first.