For the love of React, and smaller bundles. We love React, and we want to keep writing it. Redact is an alternative runtime for React apps, with a smaller footprint and synchronous rendering. Keep your components, hooks, and imports, with integrations for TanStack Start, Vite, Vinext, and Next.js.
React's APIs and component model are the reason this project exists. Redact explores a different set of runtime tradeoffs within that ecosystem, not a new direction for React or a replacement for the project. Synchronous rendering keeps the runtime small, while React's concurrent scheduling supports behavior Redact doesn't provide. The compatibility table explains those differences.
The historical performance comparisons below use Redact 0.1.0 and pinned React 19.3.0, recorded September 11, 2026. The benchmark reports retain the measured source snapshots and build inputs. Current framework release checks are recorded separately.
pnpm add @tanstack/redact// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import react from '@vitejs/plugin-react'
import { redact } from '@tanstack/redact/vite'
export default defineConfig({
ssr: { noExternal: true },
plugins: [tanstackStart(), react(), redact()],
})Bundling server dependencies lets Vite redirect their React imports too. TanStack Start setup.
// vite.config.ts
import { defineConfig } from 'vite'
import { redact } from '@tanstack/redact/vite'
export default defineConfig({
plugins: [redact()],
})Keep importing from React:
import { useState, Suspense } from 'react'
import { createRoot, hydrateRoot } from 'react-dom/client'The plugin handles client and SSR builds. It leaves the RSC environment on real React so @vitejs/plugin-rsc can keep owning Server Component serialization.
// vite.config.mjs
import vinext from 'vinext'
import { redact } from '@tanstack/redact/vinext'
export default { plugins: [vinext(), redact()] }Vinext integration with native React on the server and Redact in the browser. The release gate checks both routers against native React from an unpatched npm install, across Node, standalone output, static export, and local Workers. Setup, coverage and release checks.
// next.config.mjs
import { withRedact } from '@tanstack/redact/next'
export default withRedact({})Next.js integration for Turbopack and Webpack, with App Router and Pages Router coverage. Redact runs in the browser; React handles server rendering and Server Components. Setup and version range, coverage and release checks.
Run pnpm example:next:journey for an interactive React and Redact comparison with the same app, server actions, streaming, and navigation. Demo and browser checks.
The framework release candidate cut emitted client output in Vinext's benchmark from 154.9 KB to 115.5 KB gzip, saving 39.4 KB, or 25.4%, across six identical builds. Both runs use the same Vinext app and dependencies. The total includes lazy routes, it is not one page's download.
Production build times were effectively equal. Development startup was 2.2% faster for App Router and 4.9% faster for the large Pages fixture in this local run. This suite does not measure browser rendering, hydration, responsiveness, or production SSR throughput. Measurements, versions, and raw results.
Production ESM bundles retaining all exports from the React, JSX, DOM, and DOM-client entries:
| Runtime | Gzip bytes | Compared with React |
|---|---|---|
| React 19.3.0 | 69,162 | Reference |
| Redact, default Vite configuration | 23,311 | 66.3% smaller |
| Redact, native animation enabled | 28,159 | 59.3% smaller |
These are runtime bundles, not whole applications. Redact is measured from the 0.1.0 release build. Real apps tree-shake differently, and the runtimes have different scheduling capabilities.
Standalone Redact entries, measured separately:
| Entry / configuration | Gzip bytes |
|---|---|
| DOM client, default Vite features | 20,139 |
| DOM client, native animation enabled | 24,991 |
DOM client, nano preset |
12,466 |
| React API entry | 2,764 |
| Server entry | 8,964 |
These entries overlap, so their sizes are not additive. nano removes behavior and is not a full-compatibility preset. The expanded API surface is larger than earlier Redact, not a size reduction over previous releases. Exact configurations, inputs, and historical comparisons.
Production microbenchmarks on an Apple M5 Pro, Chrome 152, without CPU throttling: five fresh-browser blocks, five rounds each. Redact uses the built package with default Vite features. Synchronous flushes ensure updates complete before timing stops.
These timings use the frozen pre-release snapshot, before the final hydration-recovery cleanup was merged from 0.0.21. That cleanup passed the release tests; the timing experiments were not repeated.
Times are median averages per completed workload batch in milliseconds, not per component. Negative percentages mean less time. Percentages use paired measurements, not division of the rounded medians.
| Browser workload | React ms | Redact ms | Redact time vs React |
|---|---|---|---|
| Sparse state updates | 0.009 | 0.002 | -71.9% |
| Mixed-depth state updates | 0.227 | 0.093 | -59.1% |
| Keyed list reversal | 0.161 | 0.112 | -30.3% |
| Context through memo | 0.073 | 0.052 | -29.1% |
| Deep-tree prop updates | 0.012 | 0.010 | -22.4% |
| Batched state updates | 0.050 | 0.041 | -19.7% |
| Null-heavy child lists | 0.094 | 0.083 | -14.2% |
| Stable keyed rows, automatic JSX | 0.050 | 0.043 | -11.6% |
| Controlled selects | 0.142 | 0.132 | -7.6% |
| Prop updates, automatic JSX | 0.237 | 0.226 | -3.8% |
| Prop updates, classic JSX | 0.249 | 0.243 | -2.3% |
| Stable keyed rows, classic JSX | 0.045 | 0.046 | No clear difference |
| Mount and unmount | 0.149 | 0.162 | +7.5% |
| Reducer batches | 0.051 | 0.055 | +7.9% |
| String SSR in the browser | 0.038 | 0.043 | +10.4% |
| Hydration | 0.182 | 0.227 | +21.4% |
| Passive effects | 0.024 | 0.032 | +27.0% |
| Reducer updates under Suspense | 0.051 | 0.076 | +49.6% |
| Suspense retry cycles | 0.117 | 0.268 | +124.8% |
The largest ratio, Suspense retries, is about 0.15 ms of extra work per cycle here. A cycle covers 96 components, 192 reducer actions, hiding, retrying, revealing, and shared correctness checks. It does not mean interactions feel 2.25 times slower. Larger trees, repeated retries, and slower devices can make the difference matter.
Separate Node 24 measurements use production source bundles, five fresh-process blocks, and the same checked output. They exclude package startup, network latency, and streaming waterfalls.
| Server workload | React ms | Redact ms | Redact time vs React |
|---|---|---|---|
| Fully-ready readable stream | 0.175 | 0.088 | -49.4% |
| String SSR with hooks/context | 0.076 | 0.053 | -30.5% |
| String SSR, clean text | 0.057 | 0.053 | -6.9% |
| String SSR, escaped text | 0.126 | 0.124 | No clear difference |
Separate 2,400-row fixtures use normal scheduling for clicks and forced garbage collection for retained heap:
| Measurement | React | Redact, default |
|---|---|---|
| Click handler to committed DOM, median / p95 | 1.5 / 1.6 ms | 1.2 / 1.3 ms |
| Mounted retained JS heap, median | 666.4 KiB | 762.5 KiB |
| Extra DOM nodes after final unmount | 0 | 0 |
Handler-to-commit is not paint latency or field INP; retained heap is not allocation rate or proof of no leaks. Observed click durations were browser-rounded to 16 ms for both; one Redact event entry was absent, not zero.
An identical-React control measured 0.5% median absolute variation across browser workloads, with wider uncertainty in some cases. Small differences need caution. These results do not establish whole-app speedups, behavior under concurrent input, or mobile/Safari/Firefox performance. All intervals and measurements, independent audit.
This is the default Vite configuration. API availability is not a promise of every React behavior, especially concurrent behavior.
| Area | Redact behavior |
|---|---|
| Core rendering | JSX, hooks (including use, useEffectEvent, useId, and useSyncExternalStore), context, refs, memo, lazy, portals, error boundaries, and modern class lifecycles. Legacy class lifecycles are no-ops. |
| DOM and forms | Controlled inputs, reset defaults, event handling, and preservation of edits during hydration have shared React tests. |
| Suspense and hydration | Fallbacks, retained primary DOM, retries, streaming boundaries, and event replay. No priority scheduling. |
Activity |
Retains hidden DOM/state and disconnects effects, refs, and subscriptions. Insertion effects stay connected. Hidden work is synchronous, not background priority. |
| Fragment refs | Stable DOM instances with events, focus, geometry, observers, scrolling, and portal/visibility tracking. |
| Transitions | useTransition retains committed content and pending state while work suspends. useDeferredValue retains stale results. Rendering passes stay synchronous, without time slicing or interruptible component renders. |
useActionState |
Queues actions against the previous result, tracks pending actions, and sends failures to error boundaries. |
useOptimistic, useFormStatus |
Optimistic updates rebase on current state and roll back when their action finishes. Form descendants receive pending status and submitted data. |
| View transitions | ViewTransition preserves state; addTransitionType does not animate by default. Native snapshots/styles/callbacks are experimental and opt-in. Animated commits wait for the browser snapshot callback, without a concurrent scheduler. |
| Browser-only rendering | browser() requests a server-to-client Suspense fallback; onBrowserBailout reports it separately from errors. use(browser()) returns undefined on the client. |
| Resource APIs | Preload/preinit/preconnect/DNS hints and declarative styles/scripts, with document/ShadowRoot ownership and streamed stylesheet coverage. |
| SSR | renderToString, renderToStaticMarkup, readable and pipeable streams. Full Fizz prerender/resume APIs are not implemented. |
| Caching | cache returns its function, cacheSignal returns null, and unstable_useCacheRefresh is a stable client no-op. Invoking a server refresh throws. Client/DOM-server rendering is uncached. |
| React Compiler | The react/compiler-runtime memo-cache entrypoint is implemented and aliased. This is not validation of every compiler output pattern. |
| Server Components / Flight | Kept on upstream React in the RSC environment, not reimplemented by Redact. |
| Debugging | Fast Refresh preserves state for compatible edits. React DevTools supports component and hook inspection, edits, owners, error and Suspense controls, and commit profiling in development. useDebugValue labels appear in DevTools. All DevTools code is removed in production. StrictMode still avoids double invocation, and the <Profiler> callback remains a no-op. DevTools checks and timings. |
Native animation is not exhaustive visual parity. Production animation, hidden-tab behavior, broader resource/navigation races, and Safari/Firefox remain unverified. API details, upstream audit, native animation limits.
redact() uses the full preset: all optional features enabled except native animation. Turn off only behavior your app does not need:
redact({ features: { hydration: false, classComponents: false } })
redact({ features: { viewTransitions: true } }) // experimental animation
redact({ preset: 'nano', features: { context: true } })nano starts with every optional feature off. Overrides merge with the preset. Feature savings overlap and are not additive.
What each disabled feature removes
| Flag | Behavior when false |
|---|---|
activity |
Hidden children unmount and lose state. |
fragmentRefs |
No Fragment DOM instance. |
viewTransitions |
No native snapshots or animation callbacks; the boundary retains state. |
portal |
Children render in place, not into the target container. |
context |
Providers do not propagate values; reads return the default. |
suspense |
No fallback UI; thrown thenables still retry on settlement. |
memo |
No prop-equality bailout. |
forwardRef |
No wrapper forwarding; ordinary ref props still work. |
lazy |
Only already-synchronous payloads resolve. |
classComponents |
Constructor, render, and setState only; no lifecycles or error boundaries. |
hydration |
hydrateRoot throws; use createRoot. |
pnpm test:ci covers 1,859 passing tests, type checking, built-package verification, and all 38 source/dist size budgets. The 91 ordinary-suite skips are not passes. The built full runtime used by Vinext and Next.js is 27,921 bytes gzip, and the default Vite runtime is 23,304. Both fit their pre-integration limits of 28,183 and 23,335 bytes. Current size measurements.
Vinext has a 102-configuration local gate covering 1,410 scenarios, plus 282 hosted scenarios across 18 configurations. Both App Router and Pages Router run against native React controls. Vinext coverage.
Next.js has a 162-configuration gate covering six pinned releases, both bundlers, three browser engines, development and production, standalone output, and static export. Results preserve individual scenario failures and require complete coverage of the configured matrix. Coverage, measurements, and reproduction commands. Earlier release Chrome ledger.
| Real app | Published Redact 0.1.0 result |
|---|---|
| tanstack.com | Production build, 503 tests, 5 local Worker routes, navigation/history, controls, portals, and mobile checks pass. Deployed preview works; production upgrade awaits required review. |
| tannerlinsley.com | Upgraded in production. Normal build prerenders all 10 pages; 8 live routes and browser interactions pass with no browser errors. Existing mobile header overflow remains. |
These are integration checks, not site-speed measurements or coverage of authenticated flows. Published-package and deployment evidence, earlier renderer controls.
- Run
pnpm changesetfor a package change and commit the release note with your PR. - Merge the PR into
main. The Release workflow opens or updatesci: Version Packageswith the new version and changelog. - Review that version PR, approve its checks if GitHub requests it, and merge after they pass. The workflow tests, builds, publishes to npm's
latesttag, and creates a GitHub release.
Publishing uses npm trusted publishing, not a stored npm token. No local publish or hand-edited version bump is needed. Release configuration, contributor details.
pnpm install
pnpm test:ci # tests, types, build/import checks, size budgets
pnpm size # built-package sizes per entry and feature
pnpm --filter ssr-demo devAdvanced configuration and internals, benchmark reproduction, full ship review, Projecting React.