Skip to content

Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues

FixDevs ·

Quick Answer

How to fix Million.js issues — compiler setup with Vite and Next.js, block() optimization rules, component compatibility constraints, automatic mode, and debugging performance gains.

The Problem

The Million.js compiler throws during build:

npm run build
# Error: [million] Unsupported node type in block

Or a component wrapped in block() crashes at runtime:

import { block } from 'million/react';

const MyComponent = block(function MyComponent({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});
// Error: Cannot read properties of undefined (reading 'map')

Or the optimized component renders incorrectly:

const Counter = block(function Counter({ count }) {
  return <div>{count > 0 ? <span>{count}</span> : <span>Zero</span>}</div>;
});
// Renders wrong output when count changes between 0 and positive

Or Million.js doesn’t seem to make any performance difference:

No visible speed improvement after adding block()

Why This Happens

Million.js is a compiler that optimizes React components by replacing React’s virtual DOM diffing with direct DOM manipulation. It works by analyzing components at build time and generating optimized code:

  • block() has strict rules about what components can contain — the compiler needs to statically analyze JSX. Dynamic children (.map()), conditional rendering with different JSX trees (? <A/> : <B/>), and spread props aren’t always supported. Components that violate these rules crash at compile time or render incorrectly.
  • Not all components benefit from optimization — Million.js shines with components that re-render frequently with changing props (like list items, data tables, animations). Static components, components that rarely re-render, or components with complex nested children see little to no improvement.
  • The compiler plugin must be installed correctly — Million.js transforms code at build time via a Vite or webpack plugin. Without the plugin, block() is a no-op wrapper and provides zero optimization.
  • Automatic mode has broader compatibility but less control — Million.js’s automatic mode tries to optimize all components without explicit block() calls. It’s more forgiving but may miss optimizations or cause issues with incompatible components.

Fix 1: Set Up the Compiler

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 — easiest setup
    react(),
  ],
});

Next.js:

// next.config.mjs
import million from 'million/compiler';

const nextConfig = {
  reactStrictMode: true,
};

export default million.next(nextConfig, {
  auto: true,  // Auto-optimize compatible components
  // auto: { threshold: 0.05 },  // Only optimize components above 5% render time
});

Webpack (Create React App or custom):

// webpack.config.js or craco.config.js
const million = require('million/compiler');

module.exports = {
  plugins: [
    million.webpack({ auto: true }),
  ],
};

Fix 2: Manual block() Optimization

When auto mode isn’t enough or you want explicit control:

import { block } from 'million/react';

// GOOD — simple props, no dynamic children
const UserCard = block(function UserCard({
  name,
  email,
  avatar,
  isOnline,
}: {
  name: string;
  email: string;
  avatar: string;
  isOnline: boolean;
}) {
  return (
    <div className="card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
      <span className={isOnline ? 'online' : 'offline'}>
        {isOnline ? 'Online' : 'Offline'}
      </span>
    </div>
  );
});

// GOOD — list item component (where Million.js helps most)
const TableRow = block(function TableRow({
  id,
  name,
  value,
  change,
}: {
  id: string;
  name: string;
  value: number;
  change: number;
}) {
  return (
    <tr>
      <td>{id}</td>
      <td>{name}</td>
      <td>${value.toFixed(2)}</td>
      <td style={{ color: change >= 0 ? 'green' : 'red' }}>
        {change >= 0 ? '+' : ''}{change.toFixed(2)}%
      </td>
    </tr>
  );
});

// Usage — the parent handles the list, block optimizes each item
function StockTable({ stocks }: { stocks: Stock[] }) {
  return (
    <table>
      <tbody>
        {stocks.map(stock => (
          <TableRow
            key={stock.id}
            id={stock.id}
            name={stock.name}
            value={stock.value}
            change={stock.change}
          />
        ))}
      </tbody>
    </table>
  );
}

Fix 3: Understand block() Constraints

import { block } from 'million/react';

// ❌ WRONG — .map() inside block creates dynamic children
const BadList = block(function BadList({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});

// ✅ CORRECT — use For component from million/react
import { For } from 'million/react';

function GoodList({ items }: { items: Item[] }) {
  return (
    <ul>
      <For each={items}>
        {(item) => <li>{item.name}</li>}
      </For>
    </ul>
  );
}

// ❌ WRONG — conditional rendering with different JSX structures
const BadConditional = block(function BadConditional({ show }) {
  return show ? <div><span>Content</span></div> : <p>Empty</p>;
  // Different element types (div vs p) — block can't handle this
});

// ✅ CORRECT — same structure, different content
const GoodConditional = block(function GoodConditional({ show }) {
  return (
    <div>
      <span style={{ display: show ? 'block' : 'none' }}>Content</span>
      <span style={{ display: show ? 'none' : 'block' }}>Empty</span>
    </div>
  );
});

// ❌ WRONG — spread props
const BadSpread = block(function BadSpread(props) {
  return <div {...props} />;
});

// ✅ CORRECT — explicit props
const GoodExplicit = block(function GoodExplicit({
  className,
  style,
  children,
}: {
  className: string;
  style: React.CSSProperties;
  children: React.ReactNode;
}) {
  return <div className={className} style={style}>{children}</div>;
});

// ❌ WRONG — hooks inside block
const BadHooks = block(function BadHooks({ id }) {
  const [data, setData] = useState(null);  // Hooks not supported in block
  return <div>{data}</div>;
});

// ✅ CORRECT — lift hooks to parent, pass as props
function ParentWithHooks({ id }: { id: string }) {
  const [data, setData] = useState(null);
  useEffect(() => { fetchData(id).then(setData); }, [id]);
  return <OptimizedDisplay data={data} />;
}

const OptimizedDisplay = block(function OptimizedDisplay({
  data,
}: {
  data: any;
}) {
  return <div>{data?.name ?? 'Loading...'}</div>;
});

Fix 4: The For Component (Optimized Lists)

import { For } from 'million/react';

// For replaces .map() with an optimized list renderer
function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      <For each={todos}>
        {(todo) => (
          <li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </li>
        )}
      </For>
    </ul>
  );
}

// For with complex items
function DataGrid({ rows }: { rows: DataRow[] }) {
  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Value</th>
        </tr>
      </thead>
      <tbody>
        <For each={rows}>
          {(row) => (
            <tr>
              <td>{row.id}</td>
              <td>{row.name}</td>
              <td>{row.value}</td>
            </tr>
          )}
        </For>
      </tbody>
    </table>
  );
}

Fix 5: Automatic Mode Configuration

// vite.config.ts — fine-tune automatic optimization
import million from 'million/compiler';

export default defineConfig({
  plugins: [
    million.vite({
      auto: {
        // Skip specific components
        skip: ['ComplexComponent', 'LegacyWidget'],
        // Or use a threshold — only optimize components that render above this time
        threshold: 0.05,  // 50ms
      },
      // Telemetry for debugging
      telemetry: false,
    }),
    react(),
  ],
});
// Opt-out specific components from auto mode
// Add a comment to skip optimization

// million-ignore
function ComplexComponent() {
  // This component won't be optimized
  return <div>...</div>;
}

// Or use the runtime skip
import { renderReact } from 'million/react';

function AlwaysReactComponent({ data }) {
  // Force React's normal rendering path
  return renderReact(() => <ComplexTree data={data} />);
}

Fix 6: Measure Performance Gains

// React DevTools Profiler — compare before/after
// 1. Open React DevTools → Profiler tab
// 2. Record a session without Million.js
// 3. Enable Million.js and record again
// 4. Compare render times for optimized components

// Console measurement
import { block } from 'million/react';

const OptimizedRow = block(function OptimizedRow({ data }) {
  return (
    <tr>
      <td>{data.name}</td>
      <td>{data.value}</td>
    </tr>
  );
});

// Benchmark with many items
function BenchmarkTable() {
  const [items, setItems] = useState(() =>
    Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `Item ${i}`,
      value: Math.random() * 100,
    }))
  );

  function shuffleItems() {
    console.time('render');
    setItems(prev => [...prev].sort(() => Math.random() - 0.5));
    // Measure in useEffect or requestAnimationFrame
    requestAnimationFrame(() => {
      console.timeEnd('render');
    });
  }

  return (
    <div>
      <button onClick={shuffleItems}>Shuffle (10k rows)</button>
      <table>
        <tbody>
          <For each={items}>
            {(item) => <OptimizedRow key={item.id} data={item} />}
          </For>
        </tbody>
      </table>
    </div>
  );
}

Still Not Working?

“Unsupported node type in block” at build time — the component uses a JSX pattern that the Million.js compiler can’t statically analyze. Common culprits: .map() calls (use <For> instead), spread props ({...props}), function calls that return JSX, or components as variables (const Comp = condition ? A : B). Simplify the component or add a // million-ignore comment.

Component renders correctly the first time but breaks on updateblock() generates code that patches the DOM directly instead of diffing a virtual tree. If the component’s structure changes between renders (e.g., <div> becomes <span>), the patch fails. Keep the JSX structure stable — hide/show with CSS or conditional content rather than conditional elements.

No performance improvement visible — Million.js helps most with components that re-render frequently with many prop changes (data tables, charts, real-time dashboards). If your component re-renders once or twice, the overhead of normal React diffing is negligible. Profile your app first to find the actual bottlenecks before optimizing.

Auto mode causes random components to break — add the breaking components to the skip list: auto: { skip: ['BrokenComponent'] }. Auto mode analyzes components heuristically and can’t always determine compatibility. Components with complex hooks, refs, or context dependencies may not be compatible.

For related React performance issues, see Fix: React useState Not Updating and Fix: React Event Handler Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles