Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues
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 blockOr 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 positiveOr 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 millionVite:
// 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 update — block() 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: Radix UI Not Working — Popover Not Opening, Dialog Closing Immediately, or Styling Breaking
How to fix Radix UI issues — Popover and Dialog setup, controlled vs uncontrolled state, portal rendering, animation with CSS or Framer Motion, accessibility traps, and Tailwind CSS integration.