Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Preact Admin & Architecture Guide

Generated June 2026 from preactjs/preact at 850bbdb

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the deep technical reference for Preact — its architecture, internals, build pipeline, module map, security posture, code conventions, and contribution workflow. It is written for library maintainers, framework authors, integrators embedding Preact, and engineers who need to understand precisely how Preact's renderer works. Everything here is derived directly from the Preact source at the documented commit.

Important

This guide covers Preact v11.0.0-beta.1 (the main branch — the in-development next major). Internals such as the options hook names, vnode flags, and the diff algorithm differ from stable v10. For production integrations, target the v10.x branch unless you are specifically building against v11.

For the end-user/application-developer API (components, hooks, JSX, compat usage), see the companion User Guide.


Table of Contents

  1. Executive Summary
  2. Architecture Overview
  3. Repository Structure
  4. The Rendering Pipeline (Deep Dive)
  5. The options Hook System
  6. Module Reference
  7. The Compat Layer Internals
  8. Build System & Distribution
  9. Testing Strategy
  10. Code Quality & Conventions
  11. Security Analysis
  12. Integration & Deployment Concerns
  13. Known Issues & Roadmap
  14. Potential Improvements
  15. Contributing
  16. Quick Reference

1. Executive Summary

Preact is a ~4 kB Virtual DOM library that implements React's modern component API in a fraction of the size. It is not a fork of React; it is an independent, from-scratch implementation whose public surface deliberately mirrors React so that knowledge and (via preact/compat) code transfer directly.

Attribute Value
Package preact
Version (this guide) 11.0.0-beta.1 (pre-release main)
License MIT
Runtime dependencies None (core)
Language JavaScript (ES modules), shipped with hand-authored TypeScript definitions
Module formats CJS, ESM, UMD (per sub-package)
Build tool microbundle
Test runner Vitest (browser mode via Playwright/Chromium) + tsc type tests
Source size ~877 KB across 282 files; core engine ~158 KB
Homepage https://preactjs.com
Repository https://github.com/preactjs/preact

Design philosophy. Three properties drive nearly every implementation decision:

  1. Minimal size. The codebase aggressively golfs bytes — bitfield flags instead of booleans, shared EMPTY_OBJ/EMPTY_ARR singletons, null/undefined constants reused by name, and Array.prototype.some used as a short-circuitable forEach.
  2. Extensibility via options. All add-ons (hooks, compat, debug, devtools) are built by patching a small set of renderer hook points on a shared options object, rather than by forking the core. This is the single most important architectural idea in the project.
  3. React API fidelity. The public API matches React's; preact/compat closes the remaining gaps (DOM prop normalization, synthetic-event shims, defaultProps, portals, Suspense, etc.).

2. Architecture Overview

                       Application code (JSX)
                               │  compiled to h()/jsx()
                               ▼
   ┌───────────────────────────────────────────────────────────┐
   │                        preact (core)                        │
   │                                                             │
   │   create-element.js ──► VNode objects                       │
   │            │                                                │
   │   render.js / hydrate ──► diff/index.js (diff)              │
   │            │                    │                           │
   │            │                    ├─ diff/children.js (keyed) │
   │            │                    ├─ diff/props.js (DOM attrs)│
   │            │                    └─ diff/catch-error.js      │
   │            ▼                                                │
   │   component.js (BaseComponent, setState, enqueueRender,     │
   │                 microtask render queue, depth sort)         │
   │            │                                                │
   │   options.js  ◄── the extension seam (hook registry)       │
   └───────────────────────────────────────────────────────────┘
        ▲            ▲             ▲              ▲
        │            │             │              │   (each patches options.*)
   preact/hooks  preact/compat  preact/debug  preact/devtools
   (state +      (React API +   (dev warnings (DevTools
    effects)      DOM norm +     + validation)  bridge)
                  Suspense)
                               ▲
                               │
                   preact/jsx-runtime  (Babel automatic transform)
                   preact/compat/server (SSR via preact-render-to-string)
                   preact/test-utils    (act/rerender/teardown)

The core owns VNode creation, the diff/commit cycle, the asynchronous render scheduler, and the options registry. Every other package is a consumer of options — it imports options from preact and wraps the existing hook functions, chaining to the previous implementation. This is how hooks, devtools, compat, and debug all coexist and compose without modifying core.


3. Repository Structure

The repository is a single npm package that publishes multiple entry points, each with its own src/, dist/, and package.json (used only for the per-entry module fields), wired together by the root package.json exports map.

preact/
├── src/                  Core engine (h, render, diff, Component, options, context)
│   └── diff/             Diffing: index.js, children.js, props.js, catch-error.js
├── hooks/src/            preact/hooks — useState, useEffect, …
├── compat/src/           preact/compat — React compatibility layer
│   ├── suspense.js       Suspense + lazy
│   ├── portals.js        createPortal
│   ├── memo.js           memo
│   ├── forwardRef.js     forwardRef
│   ├── PureComponent.js  PureComponent
│   ├── Children.js       Children utilities
│   ├── render.js         DOM prop normalization + event shims + render proxy
│   └── hooks.js          useTransition/useDeferredValue/useSyncExternalStore shims
├── compat/               server.js, server.browser.js, scheduler.js, client.* (SSR + shims)
├── debug/src/            preact/debug — dev validation + component stacks
├── devtools/src/         preact/devtools — DevTools bridge
├── jsx-runtime/src/      preact/jsx-runtime — automatic JSX transform
├── test-utils/src/       preact/test-utils — act/rerender/teardown
├── test/                 Browser, node, ts, and shared test suites
├── demo/                 Local development demos (not published)
├── benchmarks/           Performance benchmarks (not published)
├── config/               compat-entries.js (postbuild entry generation)
├── package.json          Root manifest + exports map
├── babel.config.js       JSX transform config for tests
├── biome.json            Formatter config
├── vitest.config.mjs     Test runner config
├── mangle.json           Property-mangling rules (size optimization)
└── jsconfig*.json        Type-check configs

Note

src/, hooks/src/, compat/src/, etc. are the product. demo/, benchmarks/, and test/ dominate the file count but are development-only and excluded from the published package's files allowlist. The published artifacts are the dist/ bundles plus the src/ sources and .d.ts files.

Published entry points (exports map)

Specifier Source Notes
preact src/index.js Core. Types: src/index.d.ts.
preact/hooks hooks/src/index.js
preact/compat compat/src/index.js Aliasable as react/react-dom.
preact/debug debug/src/index.js Dev only.
preact/devtools devtools/src/index.js
preact/test-utils & preact/compat/test-utils test-utils/src/index.js
preact/jsx-runtime & preact/jsx-dev-runtime jsx-runtime/src/index.js
preact/compat/jsx-runtime, …/jsx-dev-runtime compat/jsx-runtime.* React-source JSX wiring.
preact/compat/server, …/server.browser compat/server.* SSR (delegates to preact-render-to-string).
preact/compat/scheduler compat/scheduler.* React scheduler shim.
preact/compat/client compat/client.* react-dom/client shim.

Each exports entry resolves types, module/import (ESM .mjs), umd, and require (CJS .js) conditions.


4. The Rendering Pipeline (Deep Dive)

VNode shape

createElement/h extract key and ref from props, then call createVNode, which always allocates an object of the same shape (so V8 can use a single hidden class):

const vnode = {
	type, props, key, ref,
	_children: null, _parent: null, _depth: 0,
	_dom: null, _component: null,
	constructor: undefined,       // set to undefined to prevent JSON injection
	_original: ++vnodeId, _index: -1, _flags: 0
};

constructor: undefined is a security measure: isValidElement checks vnode.constructor === undefined, so a JSON payload (whose constructor is Object) can never masquerade as a VNode. See Security Analysis.

render(vnode, parent) flow

  1. If parent === document, retarget to document.documentElement (issue #3794).
  2. Fire options._root(vnode, parent) if registered.
  3. Determine hydration mode from the vnode's MODE_HYDRATE flag.
  4. Retrieve the previous tree from parent._children (Preact stores the last-rendered tree on the container's _children property — this is how repeated render() calls update in place).
  5. Wrap the new vnode in a Fragment and store it on parent._children.
  6. Call diff(...) with a fresh commitQueue and refQueue.
  7. Call commitRoot(commitQueue, parent._children, refQueue) to apply refs and run lifecycle/effect callbacks.

hydrate(vnode, parent) simply sets the MODE_HYDRATE flag on the vnode and delegates to render.

The diff() function (src/diff/index.js)

diff() is the heart of the renderer. For a given new/old vnode pair it:

  • Bails out early when newVNode.constructor !== undefined (injection guard).
  • Resumes suspended hydration when the old vnode carries MODE_SUSPENDED | MODE_HYDRATE.
  • Function/class components: instantiates or reuses the component, runs getDerivedStateFromProps, the pre-render lifecycle methods, shouldComponentUpdate (bail-out path), then calls render(). Function components are backed by a BaseComponent whose render is a thunk (doRender) calling the function. Handles setState called during render via a loop capped at 25 iterations.
  • DOM/text nodes: delegates to diffElementNodes, which creates/locates the element, diffs props (setProperty in diff/props.js), tracks SVG/MathML namespaces, handles dangerouslySetInnerHTML, and recurses into diffChildren.
  • Fires options._diff, options._render, and options.diffed hooks at the appropriate phases.
  • Routes thrown errors (including thrown promises for Suspense) to options._catchError.

Keyed children diffing (src/diff/children.js)

diffChildren matches old and new children by key (and type), using vnode _flags (INSERT_VNODE, MATCHED) to mark insertions and matches, minimizing DOM moves on reorder. getDomSibling (in component.js) locates the correct DOM insertion point by walking the vnode tree.

Component state & the render scheduler (src/component.js)

BaseComponent provides setState and forceUpdate:

  • setState(update, callback?) clones state into _nextState (once), merges the update (calling it as a function with a cloned state if it's a function — to play nice with Immer's frozen state, issue #2716), pushes any callback to _stateCallbacks, and calls enqueueRender(this).
  • forceUpdate(callback?) sets the COMPONENT_FORCE bit (so shouldComponentUpdate is skipped) and enqueues.

enqueueRender adds the component to a module-level rerenderQueue (guarded by the COMPONENT_DIRTY bit to avoid duplicates) and schedules a flush via options.debounceRendering or queueMicrotask. The flush (process) sorts the queue by vnode depth (depthSort) so parents render before children, then re-renders each still-dirty component. New components added mid-flush (e.g. via a provider update) are kept in depth order for single-pass handling.

Note

This asynchronous, depth-sorted, microtask-batched scheduler is what makes setState calls coalesce into a single re-render per tick and guarantees top-down render order.

Commit & refs

commitRoot applies queued refs (applyRef), fires options._commit, then drains each component's _renderCallbacks (lifecycle callbacks like componentDidMount/componentDidUpdate and effect runners installed by hooks). applyRef supports both function refs (storing a returned cleanup on the function itself) and object refs (ref.current = value). unmount tears down a subtree, invoking componentWillUnmount, detaching listeners, and clearing pointers.


5. The options Hook System

options (from src/options.js) is a single shared object — initially just { _catchError }. The core calls optional hook functions on it at well-defined points; add-ons register by overwriting a hook and chaining to the previous value.

Public option hooks (Options interface)

Hook Fired when
vnode(vnode) Every time a VNode is created.
unmount(vnode) Immediately before a vnode unmounts.
diffed(vnode) After a vnode has rendered/diffed.
event(e) When a DOM event is dispatched (compat shims synthetic-event methods here).
requestAnimationFrame(cb) Scheduler override for after-paint effects.
debounceRendering(cb) Override the render-queue flush scheduler.
useDebugValue(value) Forward custom-hook debug labels to DevTools.
attr(name, value) Customize attribute serialization for the precompiled JSX transform.

Internal (underscore-prefixed) hooks

These are private but are the extension points the first-party add-ons use: _catchError, _diff (before diff), _render (before component render), _commit, _root, _hook (on each hook invocation), _hydrationMismatch, _addHookName, __suspenseDidResolve. They are mangled in production builds (see mangle.json) and are not a stable public API.

Caution

Do not depend on the internal underscore-prefixed options hooks or vnode fields from application code. They are mangled, unstable across versions, and exist solely so that hooks/compat/debug/devtools can extend core. Build on the public Options interface only.

Example of the pattern (preact/hooks installing its effect machinery):

import { options as _options } from 'preact';
const options = _options;

let oldBeforeRender = options._render;
options._render = vnode => {
	if (oldBeforeRender) oldBeforeRender(vnode);   // chain
	// …hooks-specific logic…
};

Every add-on follows this "save old, install new, call old" discipline so multiple packages can layer hooks on the same point.


6. Module Reference

Core (src/)

File Responsibility
index.js Public barrel — re-exports render, hydrate, h/createElement, Fragment, createRef, isValidElement, Component, cloneElement, createContext, toChildArray, options.
create-element.js createElement/h, createVNode, createRef, Fragment, isValidElement.
component.js BaseComponent (setState/forceUpdate), enqueueRender, render queue + process, getDomSibling, renderComponent.
render.js render/hydrate entry points.
clone-element.js cloneElement.
create-context.js createContext (provider/consumer via subscriber Set, shouldComponentUpdate-driven propagation).
options.js The shared options registry (seeded with _catchError).
constants.js Bitfield flags (MODE_HYDRATE, MODE_SUSPENDED, INSERT_VNODE, MATCHED, FORCE_PROPS_REVALIDATE, COMPONENT_*), namespace URIs (SVG/XHTML/MathML), shared NULL/UNDEFINED/EMPTY_OBJ/EMPTY_ARR singletons.
util.js isArray, slice, assign (= Object.assign), removeNode.
diff/index.js diff, commitRoot, diffElementNodes, applyRef, unmount.
diff/children.js diffChildren (keyed reconciliation), toChildArray.
diff/props.js setProperty — applies DOM attributes, events, styles.
diff/catch-error.js _catchError — walks up to the nearest error boundary / getDerivedStateFromError / componentDidCatch.
index.d.ts, dom.d.ts, jsx.d.ts, internal.d.ts Public and internal type definitions. dom.d.ts carries the exhaustive HTML/SVG attribute and event-handler type surface, including Signalish<T> for @preact/signals interop.

Hooks (hooks/src/)

index.js installs options._diff, _render, diffed, _commit, unmount, _root hooks to drive a per-component hook list (__hooks._list) and effect queue (_pendingEffects). Hook types are numbered (getHookState(index, type)): 1 useState, 2 useReducer, 3 useEffect, 4 useLayoutEffect, 5 useRef, 6 useImperativeHandle, 7 useMemo, 8 useCallback, 9 useContext, 10 useErrorBoundary, 11 useId. Effects run after paint via afterNextFrame (rAF + 35 ms setTimeout fallback for background tabs).

Diff sub-modules, compat, debug, devtools, jsx-runtime, test-utils

Covered in their dedicated sections (Compat Internals) and the User Guide. devtools/src is a thin bridge that initializes the DevTools connection and exposes addHookName. jsx-runtime/src implements jsx/jsxs/jsxDEV (all aliased to one optimized createVNode) plus the precompiled-transform helpers jsxTemplate/jsxAttr/jsxEscape.


7. The Compat Layer Internals

preact/compat (compat/src/) is where React fidelity lives. It is the most behaviorally dense add-on.

compat/src/render.js — normalization seam

Installs several options hooks:

  • options.vnode — sets vnode.$$typeof = Symbol.for('react.element') on every vnode (so react-is checks pass), applies defaultProps, moves ref from props to the vnode field for class components, and calls handleDomVNode for string-typed nodes.
  • handleDomVNode — the prop-normalization table: className ⇄ class (with a non-enumerable className getter), onChange→oninput for text input/textarea (except file/checkbox/radio), onDoubleClick→ondblclick, onFocus/onBlur→onfocusin/onfocusout, camelCase SVG props → dashed (CAMEL_PROPS regex), numeric style values → px (unless matched by IS_NON_DIMENSIONAL), defaultValue/defaultChecked fallbacks, download={true}→"", translate="no"→false, and array value for <select multiple>.
  • options.event — adds React synthetic-event shims: persist() (no-op), nativeEvent, isPropagationStopped(), isDefaultPrevented().
  • options._render/diffed — track currentComponent (for react-relay) and sync <textarea> value.
  • Component.prototype.isReactComponent = true and UNSAFE_* lifecycle aliasing via Object.defineProperty getters/setters.
  • render/hydrate proxies — accept an optional callback, return the root component reference, and clear existing container DOM (textContent = '') on first render (React parity, issues #1727/#1828).
  • Exposes __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED with a ReactCurrentDispatcher for libraries that reach into React internals.

Suspense & lazy (compat/src/suspense.js)

Suspense is a hand-rolled Component subclass (custom prototype assignment to save bytes). It patches options._catchError to intercept thrown promises: it walks up to the nearest component with _childDidSuspend, parks the suspended subtree (cloning it into a detached <div> so its effects/cleanups pause), renders the fallback, and restores + forceUpdates the real subtree when the promise resolves. During hydration it keeps the SSR markup on screen instead of swapping in the fallback. lazy(loader) returns a component that throws the load promise until it resolves, then renders exports.default || exports; it caches the promise/result/error across renders.

Other compat modules

Module Behavior
memo.js Wraps a component with a shouldComponentUpdate using shallowDiffers (or a custom comparer); sets displayName = 'Memo(...)' and _forwarded/isReactComponent.
PureComponent.js Component subclass with shallowDiffers-based shouldComponentUpdate; sets isPureReactComponent.
forwardRef.js Wraps a render fn, passing props.ref as the second arg; tags it with Symbol.for('react.forward_ref') and a render property (mobx-react relies on this).
portals.js createPortal renders into a "fake" DOM parent that proxies insertBefore to the real container, wrapped in a ContextProvider so context flows across the portal.
Children.js map/forEach/count/only/toArray over the (flattened) children — mostly passthrough to toChildArray.
hooks.js React 18 concurrency shims: startTransition runs synchronously, useDeferredValue returns its argument, useTransition returns [false, startTransition], useInsertionEffect = useLayoutEffect, and a real useSyncExternalStore implementation.

8. Build System & Distribution

Toolchain

Concern Tool
Bundler microbundle (per-package, --raw --no-generateTypes -f cjs,esm,umd)
Formatter Biome (biome format)
Linter oxlint + tsc type-checking (jsconfig-lint.json)
Tests Vitest (browser mode via Playwright Chromium) + tsc type tests
JSX (tests) Babel (@babel/plugin-transform-react-jsx)
Size tracking gzip/brotli badges via badgesize/unpkg (README)
Node version pinned to 20.19.1 via Volta

npm scripts

Script Action
build Runs all build:* in parallel (core, debug, devtools, hooks, test-utils, compat, jsx), then postbuild.
build:compat microbundle with --globals 'preact/hooks=preactHooks' (UMD global mapping).
postbuild node ./config/compat-entries.js generates the compat sub-entry files.
dev, dev:hooks, dev:compat microbundle watch mode.
test build → lint → test:unit.
test:unit Vitest (minified run) + TS type tests in parallel.
test:vitest / :min Vitest run (with COVERAGE/MINIFY env flags).
test:ts:core / :compat tsc -p against the test/ts and compat/test/ts projects.
lint oxlint + tsc.
format / format:check Biome formatting.

Property mangling (mangle.json)

To hit the 4 kB target, internal property names (the underscore-prefixed vnode/component/options fields) are renamed at build time via babel-plugin-transform-rename-properties. Each sub-package has its own mangle.json so internal names stay consistent across the core and add-ons that share the renamed shapes.

Important

Because internal fields are mangled, source-level field names (_children, _component, _diff, …) shown in this guide are the unmangled development names. Never reference them from outside the library — in a production bundle they will have short, unstable names.

Distribution artifacts

The published files allowlist ships, per entry point: dist/*.{js,mjs,umd.js}, the src/ sources, the .d.ts types, and the per-entry package.json. main/module/umd:main/source/types plus the exports map route consumers to the right artifact for their module system. publishConfig.provenance: true enables npm build provenance attestation.


9. Testing Strategy

Suite Location Tool Purpose
Browser behavior test/browser/ Vitest + Playwright Chromium The bulk of coverage — rendering, lifecycles, keys, focus, hydration, SVG/MathML, events, refs, fragments.
Lifecycle matrix test/browser/lifecycles/ Vitest Each lifecycle method individually.
Node/build artifact test/node/ Vitest + chai Asserts the built bundle exports the expected public API (createElement, h, Component, render, hydrate).
Shared test/shared/ Vitest createElement/createContext/isValidElement semantics shared across environments.
TypeScript types test/ts/, compat/test/ts/ tsc Compile-time type assertions (*.test-d.tsx) for the .d.ts surface, including JSX namespace, DOM attributes, refs, HOCs.
Per-package hooks/test/, compat/test/, debug/test/, devtools/test/, jsx-runtime/test/, test-utils/test/ Vitest Each add-on's own suite.

Tests use preact/test-utils' act/setupRerender/teardown to flush the async scheduler deterministically. Coverage is collected with @vitest/coverage-v8 and reported to Coveralls.

Tip

Tests run against the minified build by default (test:vitest:min, MINIFY=true). This catches mangling/minification regressions that a source-only test run would miss — a deliberate choice for a size-critical library.


10. Code Quality & Conventions

Byte-golfing idioms (intentional, pervasive):

  • Bitfield flags on vnode._flags and component._bits instead of multiple booleans (constants.js).
  • Array.prototype.some used as a short-circuitable iterator (returning truthy to stop).
  • Shared singletons EMPTY_OBJ, EMPTY_ARR, and named NULL/UNDEFINED constants (mangler renames them to short tokens; reusing names improves compression).
  • Same-shape VNode allocation from a single call site so V8 keeps one hidden class.
  • Custom prototype assignment (Suspense.prototype = new Component()) instead of class extends to shave bytes.

Conventions:

  • Pure ES modules, no transpiled class in core (@ts-ignore notes "we don't use TS to downtranspile").
  • Type safety via JSDoc @type/@param annotations checked by tsc (jsconfig-lint.json) — the source is .js, not .ts, but fully type-checked.
  • Formatting enforced by Biome; linting by oxlint; pre-commit via nano-staged + Husky.
  • Hand-authored .d.ts files (not generated) give precise public types, including an exhaustive DOM attribute/event surface in dom.d.ts and @preact/signals interop via the Signalish<T> type.

Complexity hotspots: src/diff/index.js (the diff function, ~330 lines, deeply branched between component and element paths, hydration, and Suspense bailouts) and src/diff/children.js (keyed reconciliation) are the highest-complexity, most safety-critical files. They are also the most thoroughly tested.


11. Security Analysis

Preact is a client-side rendering library with no network, filesystem, auth, or secret-handling surface — its threat model is XSS/DOM-injection within the rendering path. The relevant, source-grounded items:

Area Finding
JSON / prototype injection createVNode sets constructor: undefined, and isValidElement/diff reject any object whose constructor !== undefined. This prevents arbitrary JSON (whose constructor is Object) from being treated as a renderable VNode — a deliberate injection guard.
dangerouslySetInnerHTML Honored verbatim in diffElementNodes (sets dom.innerHTML). This is the primary XSS vector in any VDOM library: never pass untrusted/unsanitized HTML. Sanitize upstream.
Text escaping Normal children are set via dom.data/text nodes (not innerHTML), so interpolated strings are inherently escaped. The SSR JSX-attr serializer (jsxAttr/encodeEntities in jsx-runtime) HTML-escapes attribute values.
Trusted Types Conditional dangerouslySetInnerHTML can trip a Trusted Types policy (community issue #4934). When deploying under a strict Trusted-Types CSP, route HTML through an approved policy.
SES / lockdown Running under SES lockdown() has surfaced a TypeError (issue #5109). Test Preact explicitly if you ship into a hardened/locked-down realm.
No eval/dynamic code The core does not use eval, Function(), or dynamic import of remote code. lazy() imports modules your own code references.
Supply chain Zero runtime dependencies in core — a minimal supply-chain surface. SSR adds preact-render-to-string. npm provenance attestation is enabled (publishConfig.provenance).

Caution

The single highest-risk API is dangerouslySetInnerHTML. Treat any value flowing into it as untrusted by default and sanitize with a vetted sanitizer (e.g. DOMPurify) before render. The dangerously prefix is intentional.

Note

preact/debug's _catchError re-throws handled errors on a setTimeout to emit a window error event (so dev overlays like Next.js work). This is development-only behavior and must not ship to production.


12. Integration & Deployment Concerns

React drop-in via aliasing

Map react and react-dom to preact/compat (and react/jsx-runtime to preact/jsx-runtime) in your bundler. Most React libraries then work unchanged. Edge cases arise with libraries that depend on React-internal behavior or precise event timing — known community reports include floating-ui event processing differences (#4373), @radix-ui slider/tooltip behaviors (#3666, #3805), and jsx-email rendering (#5106). Validate ecosystem libraries against your app.

Bundler configuration

  • Ensure tree-shaking is enabled so unused sub-packages drop out.
  • Use the import/module (ESM) conditions for the smallest output; the umd build is only for script-tag usage.
  • For TypeScript drop-in, set paths for react/react-dom and the JSX jsxImportSource.

SSR deployment

Use preact/compat/server's renderToString (sync) or the streaming functions for SSR; require preact-render-to-string >= 6.5.0 for streaming. Hydrate on the client with hydrate(). Keep server and client renders deterministic to avoid hydration mismatches (use useId() for IDs).

Production hardening checklist

  • [ ] preact/debug is excluded from production bundles.
  • [ ] All dangerouslySetInnerHTML inputs are sanitized.
  • [ ] A Content-Security-Policy (and Trusted Types policy, if used) is configured and tested with Preact.
  • [ ] SSR output is deterministic (no Date.now()/Math.random() in first render).
  • [ ] preact/devtools is gated to the environments where you want DevTools.
  • [ ] React aliases (if using compat) resolve correctly in the production build, not just dev.

13. Known Issues & Roadmap

From the community issue tracker at the documented commit (representative, not exhaustive):

Confirmed bugs

Issue Summary
#5109 TypeError when SES lockdown is enabled.
#4737 <select> value update fails when set to a newly added <option> simultaneously.
#3242 Focus lost during list re-order.

React-compat parity requests

Issue Summary
#4977 Support React 19 <Activity />.
#4756 Support use(promise) in preact/compat.
#4972 useSyncExternalStore third argument (getServerSnapshot).
#4927 useEffectEvent.
#4994 JSX types reflect core + compat even when compat is unused.
#4299 Run unmount hooks during commit phase for React consistency.

Forward-looking

Issue Summary
#2621 "The road to Preact 11" — the umbrella tracking issue for the major this guide documents.
#5034 RFC: Streaming SSR hydration coordination for Preact core.
#4934 Conditional dangerouslySetInnerHTML + Trusted Types.

Important

Because this is the v11 pre-release branch, expect API churn and incomplete features relative to stable v10. Track the "road to Preact 11" issue (#2621) and the release notes before adopting in production.


14. Potential Improvements

Observations for maintainers and integrators (derived from the source, not prescriptive):

  • Core render callback parity. v11 core render/hydrate dropped the third callback argument (it now lives only in compat). Migration guidance / a codemod would smooth the v10→v11 transition for apps that used render(vnode, parent, cb).
  • JSX type split (#4994). The published types merge core and compat surfaces; splitting them would prevent compat-only APIs from type-checking in plain-Preact apps.
  • Trusted Types support. First-class Trusted-Types-aware HTML handling (beyond raw innerHTML) would help strict-CSP deployments (#4934).
  • SES compatibility. Hardening against lockdown() realms (#5109) would broaden where Preact can run.
  • Streaming hydration coordination (#5034). Bringing streaming-SSR hydration into core (rather than relying solely on preact-render-to-string) is an active RFC.

15. Contributing

Step Command
Install (pins Node via Volta, installs Playwright Chromium) npm install
Run the demo app npm run dev (microbundle watch)
Build all packages npm run build
Lint + typecheck npm run lint
Format npm run format
Full test (build + lint + unit) npm test
Unit tests only npm run test:unit
Type tests npm run test:ts

Conventions to follow when contributing:

  • Author in ES modules with JSDoc types; the source is type-checked by tsc even though it's .js.
  • Keep bundle size in mind — size is a first-class review gate (gzip/brotli badges track it).
  • Add tests under the matching suite (test/browser, per-package */test, and *.test-d.tsx for types).
  • Run Biome format and oxlint before committing (enforced via Husky + nano-staged pre-commit).
  • New renderer behavior almost always means a new or modified options hook interaction — preserve the "save old, install new, chain" pattern.

See CONTRIBUTING.md in the repository for the full process.


16. Quick Reference

Vnode flags (constants.js)

Flag Bit Meaning
MODE_HYDRATE 1<<5 Attaching to existing DOM without diffing.
MODE_SUSPENDED 1<<7 Suspended on the previous render.
INSERT_VNODE 1<<2 Needs insertion during child patching.
MATCHED 1<<1 Matched with another vnode during diff.
FORCE_PROPS_REVALIDATE 1<<0 Re-validate props (post-suspense).

Component bits

Bit Value Meaning
COMPONENT_PROCESSING_EXCEPTION 1<<0 Processing an exception.
COMPONENT_PENDING_ERROR 1<<1 Has a pending error.
COMPONENT_FORCE 1<<2 Force update (skip shouldComponentUpdate).
COMPONENT_DIRTY 1<<3 Queued for update.

Namespaces

Constant URI
SVG_NAMESPACE http://www.w3.org/2000/svg
XHTML_NAMESPACE http://www.w3.org/1999/xhtml
MATH_NAMESPACE http://www.w3.org/1998/Math/MathML

Entry-point cheat sheet

Need Import
Render UI preact
State/effects in function components preact/hooks
React API / ecosystem libs preact/compat
Dev warnings + DevTools preact/debug
DevTools in production preact/devtools
Automatic JSX transform preact/jsx-runtime
SSR preact/compat/server (+ preact-render-to-string)
Tests preact/test-utils