React was created at Facebook to solve a very specific problem: a massively complex UI with hundreds of engineers committing to the same codebase, where the DOM state would silently drift out of sync with application state. The newsfeed, ads system, and chat were all built by different teams with different patterns, and it was becoming unmanageable.
The solution Facebook reached for was a declarative programming model where UI is a pure function of state. You describe what the UI should look like, and the framework reconciles the difference. This was genuinely a great idea.
But there's a detail buried in that history: React was also designed to render to something other than the DOM from the start. The renderer is swappable by design. That is not an accident — it is the entire architectural premise. React is a UI programming model that happens to ship a DOM renderer. It is not a web framework.
The virtual DOM is constantly described as a performance optimization. This is not accurate.
The virtual DOM is an abstraction layer. React builds an in-memory tree of your component output, then diffs that tree against the previous one to compute the minimum set of DOM mutations. This is necessary because React's programming model doesn't know which DOM nodes correspond to which state changes — it has to figure it out at reconciliation time.
Svelte takes a different approach: it compiles your components to surgical DOM update instructions at build time. When a variable changes, Svelte already knows exactly which DOM node to update. There is no runtime diff. No virtual DOM overhead. The output is smaller and faster because the framework disappears at compile time.
// What you write in Svelte
let count = 0;
$: doubled = count * 2;
// What Svelte compiles to (simplified)
// Direct assignment to the specific DOM text node
// No diffing. No reconciliation.
The virtual DOM is not free. Every render allocates objects that need to be garbage collected. For most apps this cost is invisible. But it exists because of the abstraction, not in spite of it.
React Native launched in 2015 and made the architecture explicit: React is a renderer-agnostic tree reconciler. The same component model, the same hooks, the same mental model — just targeting native views instead of DOM nodes.
This is powerful. But it means every decision React makes has to work across targets. The DOM is not a first-class citizen. The web platform is one of several render targets React supports.
This has real consequences. React does not use <form> and native form submission the way the web platform intends. React does not lean on the browser's built-in navigation. React manages its own event system rather than using native DOM events directly (though this improved in React 17). These are not oversights — they are the cost of the abstraction.
When a framework like SolidJS or Qwik makes different tradeoffs, they can because they are building specifically for the web. They don't need the architecture to port to Fabric or a canvas renderer.
Server-side rendering in React works like this: render the component tree to an HTML string on the server, send it to the browser, then "hydrate" — re-run the entire component tree in the browser to attach event listeners and sync React's internal state with the DOM that already exists.
Hydration is expensive. On slow devices and large pages, the time between the browser painting the server-rendered HTML and the page becoming interactive is a noticeable gap. The HTML is there, but the JavaScript hasn't finished attaching, so clicks and inputs don't work yet.
This problem is unique to React's architecture. The browser had to render the page once on the server, then React has to re-execute the tree to rebuild its internal state. Astro's partial hydration, Qwik's resumability, and SolidStart's approach all exist specifically to avoid this double-work.
React Server Components are the current attempt to fix this. By keeping some components permanently on the server and out of the client bundle entirely, React reduces the hydration surface area. It is a meaningful improvement. But it's also React catching up to architectural decisions other frameworks made from the start.
React is the right tool in a lot of situations.
Large teams benefit from the ecosystem maturity: the tooling, the testing libraries, the component libraries, the decades of Stack Overflow answers. The component model is genuinely good for reasoning about complex UI state. Hooks solved real problems with class components. The React DevTools are excellent.
If you are building a highly interactive, stateful application — a design tool, a code editor, a dashboard with real-time data — React's programming model is a good fit. The virtual DOM overhead is irrelevant next to the complexity you are managing.
But the web has moved fast. The platform now ships native <dialog>, native CSS container queries, native view transitions, the Popover API. Frameworks that are built for the web can lean on these primitives directly. React can too, but it will always be adapting a model that was designed to be platform-agnostic.
When you reach for React on a new project, the question is not "is React good?" It is: does this project need what React provides?
A marketing site with a contact form and some interactive sections doesn't need the virtual DOM reconciler. A blog doesn't need hydration overhead. A dashboard with mostly static data and a few filters can probably be built with something that compiles away.
The best engineers are not loyal to tools. They understand what problems each tool was built to solve, and they pick accordingly.
React was built to solve Facebook's problems. It solves a lot of other problems too. Just be clear on which category your project falls into before you reach for it by default.