Performance
Tachys is designed for V8-optimized performance from the ground up.
Architecture
Object Pooling
VNode objects are pooled and reused across renders. When a VNode is unmounted, it's returned to a free list. The next h() or jsx() call pops from the pool instead of allocating. This reduces GC pressure significantly in update-heavy apps.
Bitwise Flags
VNode types and child shapes are represented as bitwise flags (SMI-safe integers) rather than string comparisons. Type dispatch in the diff algorithm uses integer bitmasking:
if ((flags & VNodeFlags.Element) !== 0) { /* element path */ }Event Delegation
All event handlers are delegated to a single root listener rather than individual DOM elements. Handler references are stored on DOM nodes via a lightweight __tachys property, avoiding closure allocation per element.
Keyed Diff Algorithm
The keyed children reconciliation uses an Inferno-style LIS (Longest Increasing Subsequence) algorithm for minimal DOM moves. A small-list fast path avoids Map/LIS overhead for lists under 32 items.
Priority-Based Scheduler
Tachys's scheduler uses three priority lanes (Sync, Default, Transition) to process updates in the right order. useSyncExternalStore uses the Sync lane for tearing prevention, normal state updates use Default, and startTransition/useDeferredValue use the Transition lane so they don't block urgent work.
Two-Phase Commit for Transitions
Transition-lane renders use a two-phase commit. The render phase walks the VNode tree and, instead of mutating the DOM directly, pushes mutations (appendChild, insertBefore, removeChild, property sets) onto a typed effect queue. The commit phase flushes the queue atomically after the render completes successfully.
The effect queue unlocks two guarantees:
- Abandonment is free. If a higher-priority update arrives mid-render, the queue is thrown away. No DOM mutation has happened yet, so no rollback is needed. Hook state and ref callbacks are also rolled back to their pre-Transition values.
- Suspense during Transition never flashes the fallback. If a component throws a promise during a Transition render, the scheduler retries when the promise resolves instead of committing the fallback.
On the Sync and Default lanes the R.collecting flag is false, so the effect queue is bypassed entirely. These lanes pay only a single branch-predicted-false load per DOM operation, which V8 folds away in the optimized code.
Fiber-Style Mid-Render Yield
Keyed and non-keyed children diffing on the Transition lane check a ~5ms time slice budget between children. When the budget is exhausted the render yields by setting R.pending and returns. The scheduler picks up the continuation on the next tick. Sync and Default renders always run to completion for predictable latency.
Bundle Size
| Entry | Min | Min+gzip |
|---|---|---|
tachys (full concurrent) | 36 KB | ~11 KB |
tachys/sync (no concurrent scheduler) | 27 KB | ~8.8 KB |
tachys/sync-core (no Suspense/lazy/ErrorBoundary/Portal) | 20 KB | ~6.8 KB |
tachys/server (SSR) | 21 KB | ~7.2 KB |
tachys/hydrate (client hydration) | 27 KB | ~8.8 KB |
tachys/compiled (runtime for babel-plugin-tachys) | 7 KB | ~2.6 KB |
tachys/jsx-runtime | 1.5 KB | ~0.7 KB |
Tips
- Use keys for dynamic lists to enable the optimized keyed diff path.
- Memoize callbacks with
useCallbackto prevent unnecessary child re-renders when passing handlers as props. - Split contexts by update frequency to minimize re-renders.
- Use
memofor components that receive complex props but re-render infrequently. - Use
startTransitionfor expensive state updates that don't need to block user input. - Use
useSyncExternalStorefor external stores to get tearing prevention with Sync-lane scheduling.