Related: JavaScript Bundle Analysis: Tree Shaking and Code Splitting Explained covers why the size difference between React and Preact matters and how bundle size translates to parse time on real hardware.
React is approximately 45KB minified and gzipped (react + react-dom combined). Preact is approximately 3KB. Both let you write JSX components with hooks. Both produce the same UI. The question “why does Preact exist” has a specific technical answer, not a vague one about bundle size. React ships features that most applications never use, and those features are not cheap. Preact ships without them. The 42KB difference is exactly those features.
What this covers: What is actually inside React’s 45KB that Preact does not include, what the API compatibility story looks like, which applications benefit most from the switch, and the one major React feature that Preact does not support at all.
What React’s 45KB actually contains
React is split into two packages: react (core API, ~7KB) and react-dom (renderer for the browser, ~38KB). The react-dom package is where most of the size lives, and it contains several systems that Preact either removes entirely or significantly simplifies.
Synthetic event system. React does not attach event listeners directly to DOM elements. It attaches a single event listener to the root of the document and uses event delegation to handle all events across the component tree. When an event fires, React’s synthetic event system normalizes it into a cross-browser compatible event object before passing it to your handler.
This system exists because in 2013 when React was created, browser event handling was inconsistent enough that normalization was necessary. Today, modern browsers are far more consistent, but the synthetic event system remains because removing it would be a breaking change.
Preact attaches event listeners directly to DOM elements using addEventListener. It calls native browser events. No normalization layer. No delegation. Most applications running on modern browsers do not notice the difference.
Legacy context API support. React ships code for both the old context API (childContextTypes, contextTypes) and the new one (createContext). Legacy code and third-party libraries that still use the old API continue to work. This backward compatibility code adds size.
Preact supports only the modern createContext API. Any library that uses the old context API will not work with Preact.
Development mode tooling. React has two bundles for every module: a development build (with warnings, error messages, and development tooling) and a production build. The bundler switches between them at build time. Even the production build retains some infrastructure for React DevTools integration.
Fiber scheduling infrastructure. React’s Fiber architecture includes a complete implementation of a cooperative scheduler, work loops, priority lanes, and the infrastructure for Concurrent Mode features. This is the machinery behind useTransition, useDeferredValue, Suspense for data fetching, and streaming SSR. It is substantial code, and it is always present in your bundle even if you use none of the concurrent features.
Preact does not have a scheduler. It processes updates synchronously. This is simpler and smaller, but it means Preact does not support Concurrent Mode features.
What Preact keeps
Preact supports essentially the full modern React component API:
- Function components with all core hooks:
useState,useEffect,useReducer,useContext,useRef,useMemo,useCallback,useLayoutEffect - JSX (same syntax, same compilation output)
createContextanduseContextforwardRefandcreateRefReact.memo(calledmemoin Preact)Suspense(for lazy-loaded components viaReact.lazy())- Error boundaries (class components only, same as React)
- Portals
For most application code, the API surface is identical. Components written for React generally work in Preact without modification.
Preact/compat: the drop-in replacement layer
For projects that want to migrate from React to Preact, or that use third-party libraries expecting React, Preact provides a compatibility layer called preact/compat.
// vite.config.ts or webpack.config.js
// Alias react and react-dom to preact/compat
export default {
resolve: {
alias: {
'react': 'preact/compat',
'react-dom': 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
};
With this alias in place, any import of react or react-dom in your code or in node_modules resolves to Preact’s compatibility layer. Libraries like React Router, React Query, Radix UI, and most of the ecosystem work without any changes to their source code.
The compatibility layer adds some size on top of the 3KB Preact core, but the total is still around 4 to 5KB, compared to React’s 45KB.
Where the performance difference shows up
On a MacBook M3 or any modern development machine, the difference between React and Preact is usually imperceptible. The extra 42KB parses in under a millisecond. This is why most large applications with fast deployment targets never feel compelled to switch.
The difference becomes measurable in specific contexts:
Low-end Android devices. JavaScript parse time scales with bundle size and with CPU speed. On a mid-range Android phone with a slower CPU, 42KB of JavaScript takes meaningfully longer to parse than it does on a development machine. Cutting 42KB from the bundle has a proportionally larger effect on slower CPUs than on fast ones.
Embedded widgets. If you are building a component that will be embedded on third-party pages (chat widgets, feedback forms, survey tools, analytics dashboards), you do not control the host page’s performance budget. The host page has its own React bundle if it is a React app. Your embedded widget cannot share that React installation. Shipping 45KB of React in your widget is a significant imposition on the host page. Shipping 3KB is not.
High-traffic marketing sites with strict performance budgets. Sites where LCP targets are aggressive and every kilobyte is audited. Trading 45KB of React for 4KB of Preact can be the difference between passing and failing a performance budget.
Micro-frontends where React cannot be shared. If different micro-frontend bundles are versioned independently and cannot share a single React installation, each bundle that requires React pays the 45KB cost. Preact’s 3KB cost is less significant to pay multiple times.
The one major missing feature: Concurrent Mode
Preact does not support React’s Concurrent Mode features. Specifically:
useTransitionandstartTransitionexist in Preact’s API but do not schedule work the way React’s scheduler does. They behave more like synchronous updates with a lower priority hint rather than truly preemptible background rendering.useDeferredValuehas limited effectiveness for the same reason.- Streaming SSR via
renderToReadableStreamis not supported. - React Server Components are not supported.
For applications that rely on useTransition to keep heavy re-renders from blocking user input, or that use React Server Components and streaming SSR, Preact is not a viable replacement.
For applications using hooks, standard data fetching (React Query, SWR), React Router, and the standard component model without concurrent features, Preact is functionally identical from the developer’s perspective.
Who uses Preact in production
Shopify uses Preact for several of its storefront components because their theme ecosystem requires JavaScript bundles that load on every merchant’s store. Reducing bundle size directly reduces Time to Interactive for millions of storefronts.
Google has several internal tools and projects built on Preact, maintained partly by Jason Miller, Preact’s creator, who works at Google. The Google Search result page has used Preact for interactive elements.
Smaller SaaS products with widget offerings frequently use Preact to keep their embeddable widgets lightweight.
The pattern is consistent: teams choose Preact when they either cannot afford React’s bundle size (constraint) or do not need its advanced runtime features (simplicity). Most large SPAs do not switch because the difference does not justify the migration risk and the loss of Concurrent Mode capabilities.
Making the decision
The decision framework:
| Situation | Recommendation |
|---|---|
| New application, no performance budget constraints | React. Larger ecosystem, better concurrent features, more future-proof. |
| New application, strict bundle size budget | Preact with preact/compat. Near-identical DX, fraction of the size. |
| Existing React app, minor performance concerns | Do not migrate. The risk-to-benefit ratio is poor. |
| Embedded widget or micro-frontend | Preact is worth evaluating. |
Need useTransition for heavy re-renders | React. Preact cannot match this. |
| Need React Server Components | React. Preact has no equivalent. |
Preact exists because React’s size is not an accident of poor engineering; it is the result of deliberate decisions to support a broad range of use cases and to maintain backward compatibility. For applications that do not need those use cases, Preact removes the cost without removing the API. Understanding what the cost actually is makes the decision straightforward.