react-dom is the standard DOM renderer for React, providing a stable and feature-complete environment for building user interfaces. preact is a lightweight alternative that offers a nearly identical API to React in a much smaller footprint, often used for performance-critical applications. inferno is a high-performance library designed for speed, utilizing a virtual DOM with optimizations for rendering updates, though it has stricter requirements for optimal performance.
react-dom, preact, and inferno all solve the same fundamental problem: efficiently updating the browser DOM in response to state changes. However, they make different trade-offs regarding API compatibility, bundle weight, and runtime performance. Let's compare how they handle component rendering, state management, and DOM hydration.
react-dom uses the standard React API. You define components using functions or classes and mount them using createRoot.
// react-dom: Standard component
import { createRoot } from 'react-dom/client';
function App({ message }) {
return <div className="app">{message}</div>;
}
const root = createRoot(document.getElementById('root'));
root.render(<App message="Hello React" />);
preact mirrors the React API closely. You can use JSX directly, and the render method is similar but imports from preact.
// preact: Compatible component
import { render } from 'preact';
function App({ message }) {
return <div className="app">{message}</div>;
}
render(<App message="Hello Preact" />, document.getElementById('root'));
inferno requires specific imports and historically encouraged using createVNode for optimization, though JSX is supported via babel plugins.
// inferno: Optimized component
import { render } from 'inferno';
function App({ message }) {
return <div className="app">{message}</div>;
}
render(<App message="Hello Inferno" />, document.getElementById('root'));
react-dom supports the full Hooks API (useState, useEffect, etc.) out of the box. It relies on the Fiber architecture for scheduling updates.
// react-dom: Hooks
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
preact includes a compatible Hooks implementation. For most use cases, it behaves identically to React, though edge cases in complex effect dependencies may differ slightly.
// preact: Hooks
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
inferno supports hooks via inferno-hooks. It is optimized for speed but requires strict adherence to rules of hooks to maintain performance gains.
// inferno: Hooks
import { useState } from 'inferno-hooks';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
react-dom is the reference implementation. All React libraries work here by default.
// react-dom: Native compatibility
import { BrowserRouter } from 'react-router-dom';
// Works without configuration
preact uses an alias preact/compat to map React imports to Preact internals. This allows using most React libraries without changes.
// preact: Compatibility layer
// webpack.config.js
resolve: {
alias: {
"react": "preact/compat",
"react-dom": "preact/compat"
}
}
// Now react-router-dom works
inferno has inferno-compat but it is less comprehensive than Preact's. Some complex React libraries may break or require patches.
// inferno: Limited compatibility
import { render } from 'inferno-compat';
// Some React libraries may not function correctly
// due to differences in internal implementation
react-dom relies on the Fiber reconciler to prioritize updates. You optimize using React.memo or useMemo.
// react-dom: Memoization
import { memo } from 'react';
const ExpensiveComponent = memo(function({ data }) {
return <div>{data}</div>;
});
preact is fast by default due to its smaller size. It also supports memo but often needs less optimization for similar results.
// preact: Memoization
import { memo } from 'preact/compat';
const ExpensiveComponent = memo(function({ data }) {
return <div>{data}</div>;
});
inferno uses flags on VNodes to skip diffing entirely if content is static. This requires developer awareness of data mutability.
// inferno: VNode Flags
import { createVNode, VNodeFlags } from 'inferno';
// Manually optimizing a static node
const vnode = createVNode(
VNodeFlags.HtmlElement,
'div',
'className',
'Static Content'
);
react-dom uses react-dom/server for streaming or static HTML generation.
// react-dom: SSR
import { renderToPipeableStream } from 'react-dom/server';
stream = renderToPipeableStream(<App />, { onShellReady() {...} });
preact provides preact-render-to-string which is synchronous and very fast.
// preact: SSR
import render from 'preact-render-to-string';
const html = render(<App />);
inferno uses inferno-server for rendering. It focuses on speed and checksum validation.
// inferno: SSR
import { renderToString } from 'inferno-server';
const html = renderToString(<App />);
While the implementations differ, all three libraries share core concepts that make migration possible.
// Shared pattern
function Component({ prop }) {
return <div>{prop}</div>;
}
useState, useEffect, and useContext.// Shared hook usage
const [val, setVal] = useState(initial);
class vs className may vary in strict modes.// Shared JSX
<div className="container">Content</div>
| Feature | react-dom | preact | inferno |
|---|---|---|---|
| Bundle Size | ๐ Large (Baseline) | ๐ชถ Very Small | ๐ชถ Very Small |
| API Compatibility | โ 100% Native | โ High (via compat) | โ ๏ธ Moderate (via compat) |
| Rendering Engine | ๐งต Fiber Reconciler | ๐ Optimized Diffing | โก Flag-Based Optimization |
| Ecosystem | ๐ Massive | ๐ Large | ๐ฑ Niche |
| SSR Approach | ๐ Streaming Support | โก Synchronous String | โก Synchronous String |
| Learning Curve | ๐ Standard | ๐ Low (if knowing React) | ๐ Medium (Optimization flags) |
react-dom is the industry standard ๐๏ธ. It is the safest bet for long-term projects, hiring, and ecosystem integration. Choose this unless you have a specific reason not to.
preact is the pragmatic lightweight alternative ๐๏ธ. It offers the best balance of size and compatibility. If you need to shave off kilobytes but keep your React code, this is the winner.
inferno is the specialist tool ๐ ๏ธ. It offers raw speed but demands more from the developer regarding optimization patterns. Use it only when you have profiled your app and identified the renderer as the bottleneck.
Final Thought: For 95% of projects, react-dom or preact will serve you best. inferno remains a powerful option for specific high-performance niches but requires more maintenance overhead.
Choose react-dom for most enterprise applications where stability, ecosystem support, and long-term maintenance are priorities. It is the safest choice for teams that need access to the widest range of third-party libraries and tools without compatibility layers. Opt for this when bundle size is not the primary constraint and you want to avoid potential edge cases found in alternative renderers.
Choose preact when you need to reduce bundle size significantly without rewriting your existing React codebase. It is ideal for widgets, embedded apps, or performance-sensitive environments where every kilobyte counts. Use the preact/compat alias to maintain compatibility with the broader React ecosystem while gaining performance benefits.
Choose inferno only if you have specific, measurable performance bottlenecks that react-dom or preact cannot resolve, and you are willing to adhere to stricter coding patterns. It is suitable for highly optimized, static-heavy interfaces where you can leverage its compilation flags and immutable data patterns. Avoid for general-purpose apps due to a smaller ecosystem and stricter API constraints.
react-domThis package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as react to npm.
npm install react react-dom
import { createRoot } from 'react-dom/client';
function App() {
return <div>Hello World</div>;
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
import { renderToPipeableStream } from 'react-dom/server';
function App() {
return <div>Hello World</div>;
}
function handleRequest(res) {
// ... in your server handler ...
const stream = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
stream.pipe(res);
},
// ...
});
}
react-domSee https://react.dev/reference/react-dom
react-dom/clientSee https://react.dev/reference/react-dom/client
react-dom/server