# cel-tui Benchmark Results

**Machine:** 12th Gen Intel Core i9-12900, ~4.4 GHz  
**Runtime:** Bun 1.3.9 (x64-linux)  
**Date:** 2026-04-05 (updated from 2026-04-04 baseline)

## Ink Comparison (caveat emptor)

These numbers compare cel-tui's end-to-end pipeline against Ink using a
visually similar tree (styled text, flexbox columns, word-wrap, nested
containers with colors — the same shape as Ink's own `benchmark/simple`).

**This is not an apples-to-apples comparison.** Ink is a React-based
framework that does significantly more work per render: JSX → React
reconciliation → Yoga layout (C++ via NAPI) → segment compositing →
ANSI output. It supports React hooks, component lifecycle, concurrent
mode, and a rich ecosystem of composable components. cel-tui is a
minimal stateless framework that builds plain JS objects and writes
cells into a flat buffer — it's a fundamentally different (and much
simpler) architecture.

The comparison is included as a rough reference point, not a claim of
superiority. Ink solves a broader set of problems; cel-tui trades
features for raw throughput.

| What                                       | cel-tui    | Ink 6.8.0                                         | Ratio     |
| ------------------------------------------ | ---------- | ------------------------------------------------- | --------- |
| **Single render (comparable tree, 80×24)** | **65 µs**  | 632 µs (`renderToString`)                         | **~10×**  |
| **1,000 re-renders**                       | **67 ms**  | 632 ms (`renderToString`) / 1,462 ms (`rerender`) | **9–22×** |
| **Projected 100k re-renders**              | **~6.7 s** | ~63 s (`renderToString`) / ~146 s (`rerender`)    | **9–22×** |

The Ink benchmark script we used is a custom `renderToString` loop (see
[How to Reproduce](#how-to-reproduce)), not Ink's own shipped benchmark,
which does 100k `rerender()` calls that also write to stdout. Both
numbers are included above.

### What each framework does per render

| Step                 | cel-tui                          | Ink                             |
| -------------------- | -------------------------------- | ------------------------------- |
| Tree construction    | Plain JS objects (~0 cost)       | React.createElement + JSX       |
| Layout               | Custom flexbox (TypeScript)      | Yoga (C++ via NAPI)             |
| Painting             | Cell buffer (typed array writes) | Segment compositor (string ops) |
| Output               | ANSI emitter (string concat)     | ANSI string builder (chalk)     |
| State reconciliation | None (stateless)                 | React reconciler                |

## cel-tui Pipeline Breakdown

### visibleWidth (character measurement)

| Input                        | Time     |
| ---------------------------- | -------- |
| ASCII short (5 chars)        | 4.1 ns   |
| ASCII (64 chars)             | 39.1 ns  |
| ASCII long (640 chars)       | 386.6 ns |
| CJK + emoji (26 graphemes)   | 26.6 ns  |
| Mixed ASCII + CJK (60 chars) | 4.9 ns   |
| ANSI escape sequences        | 3.2 ns   |
| Empty string                 | 0.1 ns   |

The fast ASCII path dominates — pure ASCII strings bypass grapheme segmentation entirely.

### Layout Engine

| Tree                          | Time     |
| ----------------------------- | -------- |
| 10 children (flat)            | 1.5 µs   |
| 100 children (flat)           | 12.0 µs  |
| 1,000 children (flat)         | 125.1 µs |
| depth=3 breadth=3 (40 nodes)  | 3.0 µs   |
| depth=4 breadth=3 (121 nodes) | 15.1 µs  |
| depth=3 breadth=5 (156 nodes) | 9.5 µs   |
| 50 styled groups (200 nodes)  | 52.6 µs  |
| 10 word-wrap paragraphs       | 2.6 µs   |
| 50 word-wrap paragraphs       | 13.1 µs  |
| Ink-comparable tree           | 1.7 µs   |
| App tree (20 messages)        | 10.1 µs  |
| App tree (100 messages)       | 39.6 µs  |

Layout scales linearly with node count (~120 ns/node for flat trees).

### Paint (cell buffer writing)

| Scenario                      | Time   |
| ----------------------------- | ------ |
| 10 children (flat, 120×40)    | 93 µs  |
| 100 children (flat, 120×40)   | 193 µs |
| 1,000 children (flat, 120×40) | 192 µs |
| Nested 121 nodes (120×40)     | 180 µs |
| 50 styled groups (120×40)     | 207 µs |
| 200 lines scrollable (120×40) | 185 µs |
| 50 word-wrapped paragraphs    | 286 µs |
| Ink-comparable (80×24)        | 45 µs  |
| App tree 20 msgs (120×40)     | 208 µs |
| 80×24 (small terminal)        | 109 µs |
| 120×40 (medium terminal)      | 196 µs |
| 200×50 (large terminal)       | 284 µs |

Paint is O(cells), not O(nodes) — 100 vs 1,000 children produce similar times because only visible cells are painted. Cost scales with terminal size.

### CellBuffer & ANSI Emission

| Operation                    | Time     |
| ---------------------------- | -------- |
| Buffer creation 80×24        | 24.7 µs  |
| Buffer creation 120×40       | 58.2 µs  |
| Buffer creation 200×50       | 117.0 µs |
| emitBuffer 80×24 (full)      | 21.4 µs  |
| emitBuffer 120×40 (full)     | 46.4 µs  |
| emitDiff 80×24 (no changes)  | 11.8 µs  |
| emitDiff 80×24 (partial)     | 11.1 µs  |
| emitDiff 80×24 (full change) | 11.2 µs  |

Differential rendering has near-constant cost regardless of change amount — dominated by the comparison scan over all cells.

### Hit Test & Focus

| Operation                    | Time    |
| ---------------------------- | ------- |
| hitTest flat 100, center     | 145 ns  |
| hitTest nested 121, center   | 41 ns   |
| hitTest app 50 msgs, center  | 108 ns  |
| hitTest miss (out of bounds) | 7.7 ns  |
| collectFocusable 100 buttons | 1.13 µs |
| collectFocusable app tree    | 0.96 µs |

### Key Parsing

| Input                 | Time   |
| --------------------- | ------ |
| Printable char 'a'    | 15 ns  |
| Ctrl+C (0x03)         | 18 ns  |
| Escape                | ~1 ns  |
| Arrow up (CSI)        | 58 ns  |
| Shift+Tab (CSI Z)     | <1 ns  |
| F5 (CSI 15~)          | 111 ns |
| Backspace (0x7F)      | 7.1 ns |
| normalizeKey "ctrl+s" | 84 ns  |

### End-to-End Pipeline

| Scenario                  | Time   | Renders/sec |
| ------------------------- | ------ | ----------- |
| Ink-comparable (80×24)    | 67 µs  | **14,900**  |
| Flat 100 lines (80×24)    | 152 µs | 6,600       |
| App tree 20 msgs (120×40) | 268 µs | 3,700       |
| Re-render no-change diff  | 61 µs  | 16,300      |
| Re-render +1 message diff | 248 µs | 4,030       |
| 1,000 re-renders batch    | 67 ms  | 14,800      |

## Ink Raw Numbers

Ink v6.8.0, visually similar tree to cel-tui's "ink-comparable"
scenario. Run with Bun 1.3.9 on the same machine. Note: the
`renderToString` benchmark was written by us (not shipped by Ink) to
isolate the render pipeline from terminal I/O.

### `renderToString` (pure pipeline, no I/O)

```
Run 1: 1,000 renders: 632.75 ms — 632 µs/render
Run 2: 1,000 renders: 632.66 ms — 633 µs/render
Run 3: 1,000 renders: 630.99 ms — 631 µs/render
```

### `rerender` (full render with React reconciler + stdout)

```
Run 1: 1,000 re-renders: 1473.63 ms — 1,474 µs/render
Run 2: 1,000 re-renders: 1461.48 ms — 1,461 µs/render
Run 3: 1,000 re-renders: 1450.79 ms — 1,451 µs/render
```

## How to Reproduce

```bash
# cel-tui benchmarks
cd cel-tui
bun install
bun run benchmarks/run-all.ts       # full suite
bun run benchmarks/e2e.bench.ts     # just the headline numbers

# Ink benchmarks
git clone https://github.com/vadimdemedes/ink.git /tmp/ink-bench
cd /tmp/ink-bench
npm install && npm run build

# Create a renderToString benchmark (Ink ships rerender-to-stdout only)
cat > benchmark/simple/bench-rts.tsx << 'BENCH'
import React from "react";
import { renderToString, Box, Text } from "../../src/index.js";

function App() {
  return (
    <Box flexDirection="column" padding={1}>
      <Text underline bold color="red">{"Hello World"}</Text>
      <Box marginTop={1} width={60}>
        <Text>
          Cupcake ipsum dolor sit amet candy candy. Sesame snaps cookie
          I love tootsie roll apple pie bonbon wafer. Caramels sesame
          snaps icing cotton candy I love cookie sweet roll.
        </Text>
      </Box>
      <Box marginTop={1} flexDirection="column">
        <Text backgroundColor="white" color="black">Colors:</Text>
        <Box flexDirection="column" paddingLeft={1}>
          <Text>- <Text color="red">Red</Text></Text>
          <Text>- <Text color="blue">Blue</Text></Text>
          <Text>- <Text color="green">Green</Text></Text>
        </Box>
      </Box>
    </Box>
  );
}

renderToString(<App />, { columns: 80 }); // warmup
const start = performance.now();
for (let i = 0; i < 1_000; i++) renderToString(<App />, { columns: 80 });
const elapsed = performance.now() - start;
console.log(`1,000 renders: ${elapsed.toFixed(2)} ms (${(elapsed / 1000).toFixed(3)} ms/render)`);
BENCH

bun run benchmark/simple/bench-rts.tsx
```
