Open-source project
preactjs/preact avatar
preactjs/preact

Preact: the 4kB claim, twelve entry points, and a main branch sitting on 11.0.0 release candidates

GitHub describes it as ⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.. The repository metadata lists JavaScript as its primary language. The metadata lists the MIT license. This article stays within the project description and details documented in the GitHub repository README.

38,873 stars2,107 forksJavaScriptMIT

At a glance

What is it?
Preact is a small Virtual DOM library that keeps the React API and adds compatibility through a preact/compat alias, published on npm as preact. The details worth reading before you adopt it are the size claim that differs between the repository description and the README, the export map whose types point into src/, and a main branch whose version is 11.0.0 while every published tag is a release candidate.
Who is it for?
Adopt Preact if bundle size is a hard constraint and your team already writes the React API, because the compat alias and the jsx-runtime entry point are what make that switch cheap. Do not adopt it if you need every React package to work untouched, since the README does not document which React entry points the alias covers.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The repository description says 3kB and the README says 4kB

Preact is described three times in three places and two of those descriptions disagree. The repository description on GitHub calls it a Fast 3kB React alternative with the same modern API. The README's own header says Fast 4kB alternative to React with the same modern API, and the description field in package.json says Fast 4kb React-compatible Virtual DOM library. So the two files a reader is most likely to open both say 4kB, and the one-line summary GitHub shows in search results says 3kB.

Nothing in the README or package.json explains how either figure is measured. There is no statement of whether 4kB means gzipped, minified, or tree-shaken, and no mention of which export is being measured, since the package publishes a core build plus a separate compat build, a debug build, a devtools build and a hooks build. The benchmarks/ directory exists at the top level, which tells you measurement is taken seriously somewhere in the project, and the README does not say which output the headline number comes from.

The practical consequence is that you cannot use the headline as a budget. If your build has a hard ceiling, measure the bundle your own entry graph produces after your own minifier, with the entry points you actually import. The number in the description is a marketing artefact of two different files, and the mismatch is a good reason to check rather than to trust.

main says 11.0.0 and the published tags are all release candidates

The version field in package.json reads 11.0.0, and the three most recent releases are 11.0.0-rc.2 on 2026-09-08, 11.0.0-rc.1 on 2026-08-26 and 11.0.0-rc.0 on 2026-08-10. The repository is not archived and the last push was on 2026-09-15, so the branch is moving, but what has been tagged is a sequence of candidates for a major version rather than a final release.

That state has a practical meaning for a library you depend on. A release candidate is the point at which API decisions are still cheaper to change than after the tag, and the version in package.json on the default branch being the final number rather than a candidate says the maintainers consider the 11.0.0 API settled on main. It does not say the final tag exists. Anyone pinning a version for production is pinning an rc, or pinning the 10.x line, and the choice between those two is a decision about which API you want to write your code against.

The release sequence also tells you the cadence. Three candidates in about a month is what a major version looks like when the work is landing in visible steps. If you adopt, pin the exact tag you test against, and re-run your suite against each candidate before moving to it, because a candidate-to-candidate change is where a major version's last adjustments happen.

The export map is the public API, and its types point into src/

package.json is where this project explains itself most precisely, and the exports field is the part to read first. The listed entry points are the root import, then ./compat, ./debug, ./devtools, ./hooks, ./test-utils, ./compat/test-utils, ./jsx-runtime, ./jsx-dev-runtime, ./compat/client, ./compat/server and ./compat/server.browser. Every one of them is a separate published file, and each is a decision about what a consumer pulls into their bundle. Importing preact/debug or preact/devtools is opt-in behaviour, not a side effect of importing preact.

The detail with the most reach is that the types field of each entry points at a source path, for example ./src/index.d.ts for the root and ./compat/src/index.d.ts for compat, while the default condition resolves to a built file such as ./dist/preact.mjs. Declarations therefore come from source paths inside the package while the runtime comes from dist. main and module both point at dist/preact.mjs as well, and source is src/index.js.

The consequence is that your editor and your bundler are reading different halves of the package. That is normal for a modern ESM package, and it also means the published artefact has to carry src/ for types to resolve, which is worth knowing if you audit what ends up in your node_modules or if a tool follows a declaration back into source it cannot parse. The publishConfig block asks for public access and provenance, so the package you pull is the one the maintainers attested to.

preact/compat is a fixed set of entry points, and the README does not enumerate them

The compatibility story is the reason most teams consider Preact at all, and it is a single alias as far as the README is concerned: extensive React compatibility via a simple preact/compat alias. In the export map that alias is not one file. It is a set of them, with ./compat for the main surface, ./compat/test-utils, and the client and server split into ./compat/client, ./compat/server and ./compat/server.browser, each with its own import, require and browser conditions.

That structure is the part to think about. A React library that imports react-dom/client, react-dom/server or react-dom/test-utils has to land on one of those mapped paths, and one that imports something outside the set has no mapping at all. The README does not document which React modules the alias covers, and it does not say what happens to a component that reaches for a React internal. The type declarations for the server entry points live in a single ./compat/server.d.ts shared by the browser and non-browser variants, so the two builds differ at the JavaScript level rather than at the type level.

The consequence for a migration is that the size win is only as good as your dependency graph. A handful of packages that import plain react and react-dom will alias cleanly. One that reaches into an unmapped path will fail at build time, which is a loud failure, and a bundler alias rule that does not cover every specifier your tree uses will produce a module resolution error rather than a subtle bug. Audit the import statements in your dependency tree before you make the switch rather than after.

The first render is one call, and the README's JSX setup is older than the package

The mechanism is short. You describe your interface as a tree of components and elements, components being functions or classes that return what their tree should output, and you hand that description to render() along with the parent DOM element. Later calls to render() reuse the existing tree and update it in place, with the library calculating the difference from the previous output so it performs as few DOM operations as it can. This is the whole first run:

js
import { h, render } from 'preact';
// Tells babel to use h for JSX. It's better to configure this globally.
// See https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#usage
// In tsconfig you can specify this with the jsxFactory
/** @jsx h */

// create our tree and append it to document.body:
render(
	<main>
		<h1>Hello</h1>
	</main>,
	document.body
);

// update the tree in-place:
render(
	<main>
		<h1>Hello World!</h1>
	</main>,
	document.body
);
// ^ this second invocation of render(...) will use a single DOM call to update the text of the <h1>

The acquisition story is thinner than that example. The package is published on npm under the name preact, with main and module both resolving to dist/preact.mjs and the build badge in the README pointing at that same file on unpkg, but the README contains no install command at all, so the command comes from your package manager rather than from the project.

One inconsistency is worth noticing, because it will cost you a config line. The example configures JSX with a /** @jsx h */ pragma and a jsxFactory note, which is how JSX was wired before the automatic runtime. The package also exports ./jsx-runtime and ./jsx-dev-runtime, which is the mechanism a current bundler uses when you have not told it otherwise. Copying the README example into a modern build means adding a pragma your toolchain would not have needed.

demo/ is a scratchpad, and files named after bugs are not examples

The demo/ directory is the clearest case of a project keeping its scratch work in the open, and it is not a documentation set. Its contents include key_bug.jsx, stateOrderBug.jsx, old.js.bak, nested-suspense/, profiler.jsx, context.jsx, fragments.jsx, contenteditable.jsx, logger.jsx, list.jsx, spiral.jsx, reorder.jsx, people/, pythagoras/, mobx.jsx, redux.jsx, reduxUpdate.jsx and redux-toolkit.jsx, alongside index.html, index.jsx, style.css and style.scss and its own package.json.

A file called key_bug.jsx is a reproduction someone was working on. old.js.bak is a file that was kept rather than deleted. A nested-suspense/ folder next to a mobx.jsx file tells you the demo application exists to try the library against other people's libraries and against the awkward cases. Reading demo/ as example code leads you to copy a bug reproducer into your project.

The contract lives elsewhere. test/ and test-utils/ are the test story, the test helpers are published as the ./test-utils and ./compat/test-utils entry points so consumers can use the same helpers, and the runner is configured through vitest.config.mjs with vitest.setup.js. The README also links a coveralls badge, so coverage is tracked. If you want to know what Preact guarantees, the test directory and the vitest configuration are where that is written down. The demo is where the maintainers work out what to guarantee.

Asynchronous rendering with a pluggable scheduler, and no documented default

One line in the README describes the scheduling model: transparent asynchronous rendering with a pluggable scheduler. Another claims a highly optimized diff algorithm and hydration straight from server-rendered markup. Both describe a renderer that does not necessarily finish its work in the same task in which you asked for it, and both point at a design decision you will only feel when a component behaves differently than you expected.

The scheduler is described as pluggable and nothing more. The README does not name the scheduler that runs by default, does not show how to supply a different one, and does not say what the queueing policy is. So a team that needs a specific scheduling strategy, a main-thread budget, or a guarantee about when a render commits has to find that on the project website rather than here. The demo/ directory includes a nested-suspense/ folder, which is where the asynchronous behaviour gets exercised against real cases.

The same silence covers hydration. Hydration is claimed, and the compat server entry points exist for rendering on a server, but no configuration option for a mismatch or a partial hydration is documented. Combined with the pluggable scheduler, that means the two places where a server-rendered app usually breaks are the two the README asserts rather than describes. If your application is server-rendered, budget time to read the documentation on the website and to test hydration against your own markup, because the claim is real and the knobs are not in this file.

No browser matrix, an ES3 companion repo, and a toolchain built around pnpm

Browser support is one line: Preact supports all modern browsers, with no version list attached. The only concrete statement about legacy targets is a note in the Getting Started section saying you do not need ES2015 to use Preact, with a link to a separate repository, preact-in-es3, that demonstrates it. The note ends by telling you to give it a try anyway. Neither the README nor package.json states which syntax level the published dist is compiled to, so if you have a hard browser baseline, inspect the built file rather than trusting the line.

The repository layout also tells you what kind of project this is. pnpm-lock.yaml and pnpm-workspace.yaml mean the repo is a pnpm workspace, src/ is the core, and compat/, debug/, devtools/, hooks/, jsx-runtime/ and test-utils/ are the sibling packages that produce the export map. The linting and formatting are not the usual pair: .oxlintrc.json and .oxfmtrc.json alongside a .prettierignore, with .husky/ for the git hooks, and jsconfig.json with a separate jsconfig-lint.js. config/, scripts/, types/, benchmarks/ and i18n/ round out the tree, and .gitmodules means submodules are part of a fresh checkout.

The licence is MIT and the primary language is JavaScript, which matters for a library rather than an application: there is no build step in your project, only a dependency resolution. That is the strongest argument for the library, and the argument against it is the compatibility surface described above.

Editorial conclusion

Adopt Preact if bundle size is a hard constraint and your team already writes the React API, because the compat alias and the jsx-runtime entry point are what make that switch cheap. Do not adopt it if you need every React package to work untouched, since the README does not document which React entry points the alias covers. Verify first by reading the export map in package.json against the React paths your dependencies import, and by checking the size of the built file you actually ship rather than the figure in the description.

Frequently asked questions

What is Preact?

Preact is a 4kB alternative to React with the same modern API, providing Virtual DOM components, JSX, HMR, SSR and DevTools. The MIT-licensed package is published on npm as preact, and main and module both resolve to dist/preact.mjs.

How do I install Preact?

The README contains no install command. The package is published on npm under the name preact with publishConfig access set to public and provenance enabled, and the build badge in the README points at https://unpkg.com/preact/dist/preact.mjs, the file that main and module resolve to.

How do I use Preact?

You describe your interface as a tree of components and elements, then call render() with that tree and a parent DOM element; later calls reuse the existing tree and update it in place. The README's second example imports useState from preact/hooks and wires it to an input's onInput handler.

What is the difference between Preact and React?

The README argues compatibility rather than difference: a familiar React API with ES6 classes, hooks and functional components, plus React compatibility through a simple preact/compat alias. Its size claim is 4kB against React, and the repository description on GitHub says 3kB, with no measurement method given for either figure.

Is Preact better than Svelte?

The README does not compare the two. It describes Preact as a 4kB alternative to React with the same modern API, built on Virtual DOM components with JSX, SSR, HMR and DevTools, and compatible with React through the preact/compat alias.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/preactjs-preact.svg)](https://hysenlabs.com/projects/preactjs-preact)