Rezi is built for speed from the ground up. Here’s how to get the most performance out of your TUI applications.
Benchmark Results
From BENCHMARKS.md (2026-02-22, PTY mode):
vs Ink
Tree Construction
Layout & Rendering
Complex UI
Scenario Rezi Ink Speedup 10 items 326μs 67ms 206x faster 100 items 326μs 26ms 80x faster 1000 items 3.07ms 141ms 46x faster
Scenario Rezi Ink Speedup Rerender 373μs 17.7ms 47x faster Layout stress 2.88ms 28ms 10x faster Scroll stress 21ms 250ms 12x faster Virtual list (100K) 985μs 22.6ms 23x faster
Scenario Rezi Ink Speedup Full UI 2.49ms 25.6ms 10x faster Strict UI 1.19ms 25.5ms 21x faster Terminal FPS stream 0.34ms 3.4ms 10x faster
vs Other Frameworks
OpenTUI React: Rezi is 2x-155x faster (geomean: ~10x)
OpenTUI Core: Rezi is faster in 19/21 scenarios (geomean: ~2.6x)
Bubble Tea: Rezi is faster in 20/21 scenarios
Rezi achieves these speeds with a binary drawlist architecture, native C renderer , and incremental layout engine . No React overhead, no DOM, no browser.
Architecture Advantages
Binary Drawlist (ZRDL)
Rezi uses a compact binary protocol for rendering:
// Traditional approach (strings)
const output = ` \x1b [31mRed text \x1b [0m` ; // ANSI escape sequences
// Rezi approach (binary)
const drawlist = builder . text ( 0 , 0 , "Red text" , { r: 255 , g: 0 , b: 0 });
const bytes = builder . build (); // Compact binary format
Benefits:
Compact: 2-10x smaller than ANSI strings
Fast: Direct memory writes, no string concatenation
Native rendering: C engine processes binary directly
Cacheable: Reuse drawlists across frames
Incremental Layout
Only dirty subtrees are re-laid out:
// Only the counter text node is re-measured
ui . column ({ gap: 1 }, [
ui . text ( "Static header" ), // Not re-laid out
ui . text ( `Count: ${ state . count } ` ), // Only this changes
ui . text ( "Static footer" ), // Not re-laid out
])
Rezi tracks which widgets changed and only recomputes layout for affected branches.
Virtual List Optimization
Virtual lists render only visible items:
ui . virtualList ({
id: "items" ,
items: state . items , // 100,000 items
itemHeight: 1 ,
h: 20 , // Only 20 visible
renderItem : ( item ) => ui . text ( item ), // Only 20 rendered
})
Performance: 1.0K ops/s on 100K items (vs Ink: 44 ops/s, 23x faster )
Optimization Patterns
Minimize State Updates
Bad: Update on every keystroke
Good: Debounce updates
app . on ( "event" , ( event , state ) => {
if ( event . action === "input" ) {
// Triggers full render on every keystroke
return { query: event . value };
}
});
Memoize Expensive Computations
Bad: Recompute every render
Good: Memoize with useMemo
const view = ( state ) => {
const sorted = state . items . sort (( a , b ) => b . score - a . score );
const filtered = sorted . filter ( item => item . active );
return ui . column ({ gap: 1 },
filtered . map ( item => ui . text ( item . name ))
);
};
Use Virtual Lists for Large Data
Bad: Render all items
Good: Virtualize
ui . column ({ gap: 1 },
state . items . map ( item => ui . text ( item )) // 10,000 items = slow!
)
Limit Frame Rate
const app = createNodeApp ({
initialState: {},
config: {
fpsCap: 30 , // Cap at 30 FPS (default: 60)
},
});
When to use:
Battery-powered devices
Remote terminals with high latency
Background processes
Performance gain: 50% CPU reduction with minimal perceived impact
Batch State Updates
Bad: Multiple updates
Good: Single update
app . update ( s => ({ count: s . count + 1 }));
app . update ( s => ({ loading: true }));
app . update ( s => ({ data: newData }));
// 3 renders!
Avoid Inline Functions
Bad: New function every render
Good: Stable references
const view = ( state ) => {
return ui . column ({ gap: 1 },
state . items . map ( item => ui . text ( item . name ))
// New arrow function every render
);
};
Memory Optimization
From benchmarks:
Framework Typical RSS Notes Rezi 80-210 MB Heap ~20-120 MB depending on tree size Ink 120-980 MB Grows significantly with tree size OpenTUI (React) 200 MB - 15 GB Memory scales poorly; OOMs at 1000 items Bubble Tea 7-10 MB Go runtime baseline, very low footprint
Reduce Memory Usage
Limit History
Limit Drawlist Cache
Clear Virtual List State
const router = createRouterIntegration ( routes , {
maxDepth: 10 , // Limit route history (default: 10)
});
const app = createNodeApp ({
initialState: {},
config: {
drawlistEncodedStringCacheCap: 65536 , // Reduce cache size
},
});
// Virtual lists maintain scroll state
// Clear when switching views
app . update ( s => ({
virtualListStates: {}, // Reset all virtual list state
}));
Prefer Column over Box
Slower: Box with no border
Faster: Direct column
ui . box ({}, children ) // Creates synthetic inner column
Use ui.box only when you need borders or backgrounds.
Minimize Wrapping
Slower: Wrapping enabled
Faster: No wrapping
ui . row ({ wrap: true }, manyChildren )
// May require 2-pass layout
Use Fixed Sizes
Slower: Auto-sizing
Faster: Fixed size
ui . box ({ border: "single" }, children )
// Measures children for intrinsic size
Reduce Overlay Count
// Bad: Many overlapping layers
ui . layers ([
ui . layer ({ id: "1" , order: 1 }, content1 ),
ui . layer ({ id: "2" , order: 2 }, content2 ),
ui . layer ({ id: "3" , order: 3 }, content3 ),
ui . layer ({ id: "4" , order: 4 }, content4 ),
]);
// Good: Minimal layers
ui . layers ([
ui . layer ({ id: "base" , order: 0 }, baseContent ),
show ( state . showModal ,
ui . layer ({ id: "modal" , order: 1 }, modalContent )
),
]);
Each layer has rendering overhead. Keep layer count minimal.
Optimize Text Wrapping
// Slow: Many wrapped text nodes
ui . column ({ gap: 1 },
longTexts . map ( text => ui . text ( text , { wrap: true , maxWidth: 80 }))
);
// Faster: Single wrapped text
const combined = longTexts . join ( " \n " );
ui . text ( combined , { wrap: true , maxWidth: 80 });
Limit Concurrent Animations
// Bad: Animate 100 items simultaneously
const animations = useStagger ( ctx , 100 , { duration: 200 });
// 100 concurrent animations = expensive
// Good: Stagger with delay
const animations = useStagger ( ctx , 100 , {
duration: 200 ,
staggerMs: 20 , // Only 10 concurrent at peak (200ms/20ms)
});
Use Container Transitions
// Slower: Hook-based animation
const AnimatedBox = defineWidget (( props , ctx ) => {
const x = useTransition ( ctx , props . x );
return ui . box ({ position: "absolute" , left: x . value }, children );
});
// Faster: Declarative transition
ui . box ({
position: "absolute" ,
left: state . x ,
transition: { duration: 200 }, // Automatic animation
}, children );
Container transitions are optimized and skip hook overhead.
Profiling
const app = createNodeApp ({
initialState: {},
config: {
internal_onRender : ( metrics ) => {
console . log ( `Render: ${ metrics . renderMs . toFixed ( 2 ) } ms` );
console . log ( `Layout: ${ metrics . layoutMs . toFixed ( 2 ) } ms` );
console . log ( `Commit: ${ metrics . commitMs . toFixed ( 2 ) } ms` );
console . log ( `Total: ${ metrics . totalMs . toFixed ( 2 ) } ms` );
},
},
});
Debug Panel
import { debugPanel } from "@rezi-ui/core" ;
ui . column ({ gap: 1 }, [
debugPanel ({ position: "top-right" }), // FPS counter + stats
// ... your UI
]);
Benchmark Your App
import { perfMarkStart , perfMarkEnd , perfSnapshot } from "@rezi-ui/core" ;
const token = perfMarkStart ( "expensive-operation" );
// ... expensive code ...
perfMarkEnd ( token );
const stats = perfSnapshot ();
console . log ( `Operation took ${ stats . phases . get ( "expensive-operation" ) } ms` );
Best Practices
Virtual Lists Always use ui.virtualList for lists with >100 items. It’s optimized for massive datasets.
Batch Updates Group state changes into single app.update() calls. Multiple updates = multiple renders.
Memoization Use ctx.useMemo() and ctx.useCallback() for expensive computations and stable references.
Frame Rate Cap FPS at 30 for background processes and remote terminals. 60 FPS is often overkill.
Next Steps
Debugging Debug and troubleshoot your TUI applications
Testing Write tests for your TUI components