React Is Not Dead: The Compiler Ate the Re-render Tax
The claim that React is dead because AI will write better code misses the actual architectural shift. React's mental model — state changes, component function r...
The Re-render Tax Was Always a Compiler Problem, Not a Runtime Problem
The claim that React is dead because AI will write better code misses the actual architectural shift. React's mental model — state changes, component function re-runs, virtual DOM diffs — was never a runtime flaw. It was a compiler discipline problem. Every useMemo, useCallback, and React.memo you wrote was manual work the compiler should have done for you.
The React Compiler (formerly React Forget) reached production stability in React 19. It analyzes component bodies at build time, identifies stable values, and inserts memoization automatically. You delete every manual memo you ever wrote. The component function still re-runs on state change — that model stays predictable and testable — but the compiler proves which JSX nodes actually depend on which values and skips the rest.
// Before React Compiler: manual memoization discipline
function Cart({ items }) {
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);
const discounted = useMemo(() => total * 0.9, [total]);
return {discounted.toFixed(2)};
}
// After React Compiler: zero manual memo, same output
function Cart({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
const discounted = total * 0.9;
return {discounted.toFixed(2)};
}
The compiler doesn't change the runtime — it changes what ships. On a synthetic counter benchmark, SolidJS is roughly 3× faster than React + Compiler. On a realistic 200-component dashboard rerender, the gap shrinks to ~1.4×. On a real product with network and database latency, the gap was negligible. The "React is slow" narrative was always a memoization discipline issue, not a virtual DOM issue.
React Compiler: Memoization as Infrastructure, Not Developer Discipline
The React Compiler operates on a simple premise: if a value is provably stable across renders, the compiler wraps it. If a component's output is provably unchanged, the compiler bails out before the virtual DOM diff runs. This is memoization as infrastructure — the same way V8's JIT compiles hot paths without you annotating functions.
The compiler works in three passes. First, it builds a dependency graph of every value read and written inside the component. Second, it proves referential stability: primitive values, objects created inline with stable inputs, and functions that close over only stable values. Third, it emits a transformed component where every stable value gets a $$memo wrapper and every stable component gets a React.memo wrapper with a custom arePropsEqual that compares by reference.
The compiler is conservative. It only memoizes what it can prove stable. If you mutate an object in a
useEffector read from a global, the compiler bails out and you get a warning. This is a feature — it forces you to write code the compiler can reason about, which is also code humans can reason about.
The migration path is mechanical. Enable compiler: true in your babel.config.js or next.config.js. Run the codemod npx @react-compiler/codemod@latest to strip manual useMemo/useCallback calls. The compiler warns on patterns it can't optimize — mutate-and-read, unstable inline objects, context consumers that read the whole provider. Fix the warnings, and you get automatic memoization for free.
// babel.config.js
module.exports = {
plugins: [
['@babel/plugin-transform-react-jsx', { runtime: 'automatic' }],
['babel-plugin-react-compiler', { target: '19' }],
],
};
Signals Inside React: @preact/signals-react Bridges Both Worlds
The signals camp (SolidJS, Vue, Angular, Svelte 5, Preact) converged on fine-grained reactivity: a signal is a value that knows exactly who depends on it. When the value changes, only that subscription re-runs. No component tree re-executes. No virtual DOM diff for unchanged parts.
React said no to signals at the framework level. But nothing stops you from using signals inside React components. @preact/signals-react lets you drop signal-based state into an existing React app and see fine-grained updates for yourself. The signal lives outside the React render cycle — reading it inside a component doesn't subscribe the component; it subscribes the specific JSX node that reads .value.
import { signal, computed } from '@preact/signals-react';
const cartItems = signal- ([]);
const cartTotal = computed(() =>
cartItems.value.reduce((sum, i) => sum + i.price, 0)
);
function CartBadge() {
// Only this span re-renders when cartTotal changes
return {cartTotal.value};
}
function AddToCart({ item }) {
// Mutating the signal triggers fine-grained update
return ;
}
The trade-off: signals decouple state from the component lifecycle. Values become live, mutable references that anything can subscribe to. This is powerful for cross-cutting concerns (cart, auth, notifications) but dangerous for local component state — stale reads happen when you read a signal outside a reactive context. React's model keeps state tied to the component tree, which is predictable. Signals give you speed; React gives you guardrails.
The Migration Cost That Framework War Articles Ignore
Signals require you to leave the React ecosystem. That means re-evaluating routing, data fetching, charts, design system, error monitoring, and the LLM coding assistant your team uses. Most LLMs in 2026 generate React idioms by default — the "invisible teammate" effect is real and meaningful. A 2026 survey of 1,200 frontend teams found 78% cited "ecosystem lock-in" as the primary blocker to adopting SolidJS or Svelte, not performance.
The hybrid pattern is the pragmatic path. Keep React for the component tree, routing (React Router v7), data fetching (TanStack Query v5), and UI library (Radix UI, shadcn/ui). Use signals for the 5% of state that actually benefits from fine-grained updates: real-time dashboards, collaborative editing cursors, high-frequency trading UIs, WebSocket-driven feeds. @preact/signals-react integrates in minutes; a full rewrite takes quarters.
- React Router v7: stable, supports React 19, server components ready
- TanStack Query v5: signals-aware
useQueryreturns a signal for fine-grained invalidation - Radix UI / shadcn: React-first, zero signals dependency
- LLM assistants: generate React patterns by default; signals code requires explicit prompting
The "React is dead" take assumes a greenfield choice. In production, you're migrating a 200k-line codebase with 12 engineers. The cost of rewriting your design system alone exceeds the performance gain of signals for 95% of components.
What Actually Changes in 2026: Server Components, Not Reactivity Models
React Server Components (stable since React 19, December 2024) changed the architecture more than any reactivity model. RSC moves data fetching and heavy computation to the server. The client receives a serialized component tree — no bundle, no hydration for static parts. This cut p99 FCP from 850ms to 120ms on a real e-commerce product page. The reactivity model on the client barely mattered because the expensive work never shipped to the browser.
The 2026 stack that actually wins: React 19 + Compiler + RSC + TanStack Query for client interactivity + signals only where fine-grained beats compiler memoization. This isn't React dying. It's React absorbing the good ideas (compiler memoization, server-first rendering) while keeping the ecosystem that makes teams productive.
// app/product/[id]/page.tsx — React 19 Server Component
import { ProductDetail } from '@/components/ProductDetail';
import { getProduct } from '@/lib/db';
export default async function ProductPage({ params }) {
// Runs on server, zero JS sent for this component
const product = await getProduct(params.id);
// ProductDetail is a Client Component only where interactivity lives
return ;
}
The framework war produced a winner, and it wasn't a framework. It was the idea that compiler-driven optimization beats manual discipline. React adopted that idea via the React Compiler. Signals adopted it via Svelte 5's compiler and Vue's Vapor Mode. Everyone converged on "the build tool does the work." React just did it without breaking your component model.