Skip to main content
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

ScenarioReziInkSpeedup
10 items326μs67ms206x faster
100 items326μs26ms80x faster
1000 items3.07ms141ms46x 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

app.on("event", (event, state) => {
  if (event.action === "input") {
    // Triggers full render on every keystroke
    return { query: event.value };
  }
});

Memoize Expensive Computations

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

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

app.update(s => ({ count: s.count + 1 }));
app.update(s => ({ loading: true }));
app.update(s => ({ data: newData }));
// 3 renders!

Avoid Inline Functions

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:
FrameworkTypical RSSNotes
Rezi80-210 MBHeap ~20-120 MB depending on tree size
Ink120-980 MBGrows significantly with tree size
OpenTUI (React)200 MB - 15 GBMemory scales poorly; OOMs at 1000 items
Bubble Tea7-10 MBGo runtime baseline, very low footprint

Reduce Memory Usage

const router = createRouterIntegration(routes, {
  maxDepth: 10,  // Limit route history (default: 10)
});

Layout Performance

Prefer Column over Box

ui.box({}, children)  // Creates synthetic inner column
Use ui.box only when you need borders or backgrounds.

Minimize Wrapping

ui.row({ wrap: true }, manyChildren)
// May require 2-pass layout

Use Fixed Sizes

ui.box({ border: "single" }, children)
// Measures children for intrinsic size

Rendering Performance

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 });

Animation Performance

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

Enable Performance Tracking

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

Build docs developers (and LLMs) love