React Compiler vs Million.js vs Preact Signals: Best React Performance (2026)
React's re-rendering model is its biggest performance footprint. Three different solutions tackle this problem: React Compiler (auto-memoization), Million.js (virtual DOM replacement), and Preact Signals (fine-grained reactivity). Here's how they compare.
Quick Comparison
| Feature | React Compiler | Million.js | Preact Signals |
|---|---|---|---|
| Approach | Auto-memoization at build | Replace virtual DOM | Fine-grained reactivity |
| Official React | Yes (Meta) | No (community) | No (Preact team) |
| Setup | Babel plugin | Babel plugin + compiler | Package install |
| Code changes | None (automatic) | Block components | Signal primitives |
| Performance gain | 2-10x fewer re-renders | 70%+ faster DOM updates | Surgical updates |
| React compatibility | 100% | ~95% | ~90% |
| Production ready | Yes (2026) | Experimental | Stable |
| Bundle impact | ~0 (build-time) | ~4KB | ~1KB |
React Compiler: The Official Solution
React Compiler (formerly React Forget) automatically adds memoization to your React components at build time. No code changes required.
How It Works
React Compiler analyzes your component code and automatically inserts useMemo, useCallback, and React.memo equivalents where beneficial. You write normal React code; the compiler optimizes it.
// You write this (normal React):
function TodoList({ todos, filter }) {
const filtered = todos.filter(t => t.status === filter);
return filtered.map(t => <TodoItem key={t.id} todo={t} />);
}
// Compiler produces optimized version that:
// - Memoizes `filtered` (only recomputes when todos/filter change)
// - Memoizes TodoItem renders (skips unchanged items)
Strengths
- Zero code changes. Add the Babel plugin and your entire app is optimized.
- Official Meta support. This is React's future. Instagram uses it in production.
- Correct by default. The compiler understands React's rules and applies safe optimizations.
- No learning curve. Your team doesn't need to learn new APIs.
- Works with existing code. Drop-in optimization for any React codebase.
Weaknesses
- Compilation time. Adds to build time (typically 10-20%).
- Not a silver bullet. Reduces unnecessary re-renders but doesn't change React's fundamental model.
- Debugging. Optimized code can be harder to trace in devtools.
- Rules of React. Your code must follow React's rules (pure render functions, proper hook usage). Non-compliant code won't be optimized.
Best For
Every React project. If you're on React 19+, enable the compiler. There's almost no downside.
Million.js: Virtual DOM Replacement
Million.js takes a radical approach — it replaces React's virtual DOM diffing with a faster block-based approach for specific components.
How It Works
Million.js identifies components that can use its "block" optimization — static structure with dynamic values — and bypasses React's virtual DOM entirely for those components.
import { block } from 'million/react';
// Wrap performance-critical components
const TodoItem = block(function TodoItem({ todo }) {
return (
<div className="todo-item">
<span>{todo.title}</span>
<span>{todo.status}</span>
</div>
);
});
Strengths
- Dramatic speedups. Up to 70% faster DOM updates for block-compatible components.
- Great for lists. Rendering large lists (1000+ items) is where Million.js shines.
- Compiler mode. Automatic block detection without manual wrapping.
- Small footprint. ~4KB gzipped.
- Easy to adopt. Wrap components selectively. No all-or-nothing.
Weaknesses
- Limited compatibility. Not all components can be "blocked." Dynamic children, refs, and complex patterns may not work.
- Experimental. Still evolving. Not recommended for production-critical paths.
- Extra dependency. Another build tool in your chain.
- Diminishing returns with React Compiler. If React Compiler eliminates most re-renders, Million.js provides less incremental benefit.
Best For
Applications with very large lists or tables where rendering performance is measurably impacting UX. Benchmark before adopting.
Preact Signals: Fine-Grained Reactivity
Signals bring fine-grained reactivity to React — only the specific DOM nodes that use a signal's value update when it changes. No component re-renders at all.
How It Works
import { signal, computed } from '@preact/signals-react';
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
// This component NEVER re-renders
// Only the text node updates when count changes
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>+1</button>
</div>
);
}
Strengths
- Surgical precision. Only the specific text node or attribute updates. Zero component re-renders.
- Simple mental model. Create a signal, use it anywhere. No memo, no useCallback, no dependency arrays.
- Tiny bundle. ~1KB gzipped.
- Global state built-in. Signals work across components without Context or state management libraries.
- Computed values. Derived state that auto-updates. No useMemo needed.
Weaknesses
- Not official React. Uses React internals in ways the React team hasn't endorsed.
- Compatibility concerns. May break with future React updates.
- Different paradigm. Team members need to learn reactive programming concepts.
- Limited ecosystem. Third-party React libraries expect useState/useReducer, not signals.
- React Compiler conflict. The compiler optimizes the useState model. Signals bypass it entirely.
Best For
Performance-critical components where even memoized re-renders are too expensive. Real-time dashboards, financial tickers, and high-frequency update UIs.
Performance Comparison
For a list of 10,000 items with frequent updates:
| Operation | Plain React | React Compiler | Million.js | Preact Signals |
|---|---|---|---|---|
| Initial render | 120ms | 110ms | 80ms | 100ms |
| Single item update | 15ms | 5ms | 2ms | 0.1ms |
| Filter change | 80ms | 30ms | 25ms | 15ms |
| Scroll (virtualized) | 8ms | 6ms | 4ms | 3ms |
Note: These are illustrative. Real performance depends on your specific component structure and update patterns. Always benchmark your actual app.
Can You Combine Them?
| Combination | Works? | Recommended? |
|---|---|---|
| React Compiler + Million.js | Yes | Maybe (diminishing returns) |
| React Compiler + Signals | Partially | No (conflicting models) |
| Million.js + Signals | Yes | Only if you understand both |
Recommendation: Start with React Compiler (free optimization). Only add Million.js or Signals if profiling shows specific bottlenecks that the compiler doesn't address.
Decision Framework
- Start with React Compiler. Enable it. Free performance. No code changes.
- Profile your app. Use React DevTools Profiler. Find actual bottlenecks.
- If large lists are slow → evaluate Million.js for those specific components.
- If high-frequency updates are slow → evaluate Signals for those specific components.
- If everything is fine → stop. Don't optimize what isn't broken.
FAQ
Do I need any of these for a typical app?
React Compiler: yes, enable it (free optimization). Million.js and Signals: probably not unless you have measurable performance issues.
Will React Compiler make useMemo and useCallback obsolete?
Effectively, yes. The compiler adds them automatically where beneficial. You can stop writing them manually.
Are Signals the future of React?
Unlikely. The React team has chosen compilation over reactive primitives. Signals work well but aren't on React's official roadmap.
Does Million.js work with Next.js?
Yes, with configuration. The compiler plugin integrates with Next.js's Babel/SWC pipeline.
The Verdict
- React Compiler: Enable on every project. Free optimization, zero effort.
- Million.js: Consider for extreme list/table performance after profiling.
- Preact Signals: Consider for real-time, high-frequency update UIs after profiling.
For 95% of React apps in 2026, React Compiler alone is sufficient. The other tools are specialized solutions for edge cases that most apps never hit.