THE RENDERING
PIPELINE
From React JSX to ANSI escape codes. Every frame passes through five stages. Each one is designed to do as little work as possible.
React Reconciler
JSX to Element Tree
Storm uses a custom React reconciler built on the react-reconciler package. There is no DOM. No browser. The reconciler maps JSX elements to internal TuiElement and TuiTextNode objects -- a lightweight, normalized tree representation designed for terminal rendering.
When React reconciles, commitUpdate replaces an element's props object entirely rather than mutating it in place. This means the framework can use reference equality (===) to detect dirty nodes during the paint phase -- if the props object is the same reference, nothing changed.
After React finishes its commit phase, resetAfterCommit triggers the paint pipeline. This is the handoff point between React's world and Storm's rendering engine. React 19's custom reconciler has a known issue where setState from external contexts (stdin, timers) does not flush automatically. Storm works around this with forceReactFlush(), which recreates the root element and calls updateContainerSync + flushSyncWork to synchronously process all pending state updates.
Layout Engine
Constraint Solving
A pure TypeScript flexbox and CSS Grid implementation. No Yoga. No native dependencies. No compilation step. The layout engine takes the element tree and computes a LayoutResult for every node: position (x, y), dimensions (width, height), inner bounds (accounting for padding), and content overflow dimensions for scroll.
Flexbox supports the full spec: flex-grow, flex-shrink, flex-basis, flex-wrap, gap, six alignment modes for alignItems (start, center, end, stretch, baseline, auto), six modes for justifyContent (start, center, end, space-between, space-around, space-evenly), margin auto centering, padding on all four sides, min/max constraints, percentage sizing, aspect ratio, and RTL direction.
CSS Grid adds gridTemplateColumns, gridTemplateRows, gridAutoFlow (row, column, dense), and gridColumn/gridRow placement. Layout uses dirty flags for incremental computation -- only changed subtrees are recalculated. The entire engine is approximately 1,561 lines of constraint solving.
Cell Buffer
Typed Array Storage
The screen is represented as flat typed arrays: Int32Array for foreground and background colors, Uint8Array for attributes, Int32Array for underline colors, and string[] for characters. This is explicitly not an object-per-cell approach. There are no Cell objects allocated during rendering. Zero GC pressure in steady state.
Each cell occupies a fixed index in the flat array: index = y * width + x. A single cell stores: 1 character (supporting full Unicode via grapheme segmentation), 1 foreground color (24-bit RGB packed into Int32), 1 background color (24-bit RGB), 1 underline color, and 1 attribute byte encoding bold, dim, italic, underline, strikethrough, and inverse via bitflags.
The buffer is double-buffered. The DiffRenderer maintains two ScreenBuffer instances (bufferA and bufferB) and alternates between them. Buffer swap is a pointer exchange using copyFrom(), not a full reallocation. On terminal resize, only the mismatched buffer is reallocated. For a 200x50 terminal: 10,000 cells across five typed arrays -- roughly 130KB total, compared to ~500KB for an equivalent object-per-cell design.
Diff Engine
Minimal Output Generation
The diff engine compares the front buffer (current frame) against the back buffer (previous frame) cell by cell. It uses a row-level fast path: if an entire row is identical via rowEquals(), it is skipped entirely with zero per-cell overhead. On a typical frame, the vast majority of rows fall into this path.
For rows that do have changes, the engine identifies runs -- maximal sequences of consecutive columns where at least one cell property differs. It then applies gap merging: if two runs are separated by fewer than 4 unchanged cells, they are merged into a single write. Emitting a few extra characters is cheaper than the cursor-repositioning escape sequence that would otherwise be needed.
SGR state tracking avoids redundant ANSI attribute codes: the renderer remembers the last-emitted foreground, background, and text attributes, and only emits escape sequences when they differ from the previous cell. The engine also uses an adaptive WASM/TypeScript strategy: WASM is used when 30% or fewer rows changed (the scroll case, where boundary-crossing overhead is amortized), while the TypeScript path wins on full repaints. For pure scroll operations, Storm detects them and uses DECSTBM (DEC Set Top and Bottom Margins) to trigger the terminal's native hardware scroll regions.
Terminal Output
Batched ANSI Sequences
The final stage converts the diff engine's output into a single batch of ANSI escape sequences. Every frame is wrapped in synchronized output (DEC private mode 2026: CSI ?2026h to start, CSI ?2026l to end). This tells the terminal emulator to buffer the entire frame and display it atomically, preventing the visual tearing you get when escape sequences arrive mid-refresh.
Cursor position is optimized: the renderer tracks the last cursor position and emits relative moves rather than absolute positioning when it is cheaper. For each run of changed cells, it emits a cursor-position command, followed by the minimal SGR (Select Graphic Rendition) changes, followed by the character data. The entire frame is built as a string array, joined once, and written with a single stdout.write() call.
Storm also supports OSC 8 hyperlinks (clickable URLs in supported terminals), OSC 11 background color control (setting the terminal's native background to match the app), and OSC 99 (kitty notification protocol) for accessibility announcements. Link ranges are populated during the paint phase and consumed by the diff renderer, which brackets them with the correct OSC 8 open/close sequences.
MEMORY
ARCHITECTURE
Typed arrays instead of objects. Zero allocation per frame. Designed for steady-state rendering with no GC pauses.
Object-per-Cell (Traditional)
Typed Arrays (Storm)
Memory Budget by Terminal Size
Double-buffered totals (2 ScreenBuffers). Five typed arrays per buffer: chars (string[]), fgs (Int32Array), bgs (Int32Array), attrArr (Uint8Array), ulColors (Int32Array).
| Terminal | Cells | Single Buffer | Double Buffer | Object-per-Cell |
|---|---|---|---|---|
| 80 x 24 | 1,920 | ~25KB | ~50KB | ~96KB |
| 200 x 50 | 10,000 | ~65KB | ~130KB | ~500KB |
| 300 x 100 | 30,000 | ~195KB | ~390KB | ~1.5MB |
At 60fps, a 200x50 terminal processes 600,000 cells per second with zero garbage collection. The buffer comment in the source says it all: "This eliminates ~30,000 Cell objects per buffer (300x100 terminal), reducing GC pressure by ~90%."
PLUGIN
ARCHITECTURE
Lifecycle hooks, dependency sorting, error isolation, inter-plugin communication, and custom element registration. Ship plugins as npm packages.
Lifecycle Hooks
Every plugin goes through a defined lifecycle. Async setup initializes resources. beforeRender and afterRender bracket each paint. cleanup runs in reverse order with double-cleanup protection.
PluginBus
Inter-plugin communication via pub/sub. Plugins emit data on named channels and subscribe to channels from other plugins. All handlers are called synchronously. Errors in handlers are caught and logged to stderr without crashing the bus.
Dependency Ordering
Plugins declare dependencies by name. The PluginManager performs topological sorting to determine initialization order. Setup runs sequentially, respecting dependencies. Missing dependencies are validated before any plugin initializes.
Custom Element Registration
Plugins can register custom element types with full lifecycle hooks: paint (receives buffer coordinates and React props), mount/unmount (tree insertion/removal), update (prop changes from reconciliation), and onKey (keyboard input when focused).
Scoped Plugins
Plugins can be scoped to specific subtrees via pushScope/popScope. A scoped plugin only runs its hooks for components within its scope boundary. This allows different parts of the UI to have different plugin configurations without global interference.